diff --git a/src/services/content.js b/src/services/content.js index 17a75ad..12eb44f 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -216,8 +216,8 @@ class ContentService { } } - /** - * ✅ multipart/form-data 형식으로 수정된 SNS 콘텐츠 생성 + /** + * ✅ 완전한 SnsContentCreateRequest DTO에 맞춘 SNS 콘텐츠 생성 * @param {Object} contentData - 콘텐츠 생성 데이터 * @returns {Promise} 생성된 콘텐츠 */ @@ -225,7 +225,7 @@ class ContentService { try { console.log('🤖 SNS 콘텐츠 생성 요청:', contentData) - // ✅ Java 백엔드 필수 필드 검증 (SnsContentCreateRequest 기준) + // ✅ 필수 필드 검증 if (!contentData.storeId) { throw new Error('매장 ID는 필수입니다.') } @@ -238,46 +238,80 @@ class ContentService { throw new Error('콘텐츠 제목은 필수입니다.') } - // ✅ FormData 생성 (multipart/form-data) + // ✅ FormData 생성 const formData = new FormData() - // ✅ request JSON 부분 구성 (Java SnsContentCreateRequest DTO에 맞춤) + // ✅ 완전한 SnsContentCreateRequest DTO에 맞춘 데이터 구성 const requestData = { - storeId: contentData.storeId || 1, + // ========== 기본 정보 ========== + storeId: parseInt(contentData.storeId), storeName: contentData.storeName || '샘플 매장', storeType: contentData.storeType || '음식점', platform: this.normalizePlatform(contentData.platform), - title: contentData.title, + title: String(contentData.title).trim(), + + // ========== 콘텐츠 생성 조건 ========== category: contentData.category || '메뉴소개', requirement: contentData.requirement || contentData.requirements || `${contentData.title}에 대한 SNS 게시물을 만들어주세요`, target: contentData.target || contentData.targetAudience || '일반고객', - contentType: contentData.contentType || 'sns', - eventName: contentData.eventName || null, + toneAndManner: contentData.toneAndManner || '친근함', + emotionIntensity: contentData.emotionIntensity || contentData.emotionalIntensity || '보통', + + // ========== 이벤트 정보 ========== + eventName: contentData.eventName || '', startDate: this.convertToJavaDate(contentData.startDate), - endDate: this.convertToJavaDate(contentData.endDate) + endDate: this.convertToJavaDate(contentData.endDate), + + // ========== 미디어 정보 ========== + images: [], // 파일로 별도 전송 + photoStyle: contentData.photoStyle || '밝고 화사한', + + // ========== 추가 옵션 ========== + includeHashtags: contentData.includeHashtags !== false, + includeEmojis: contentData.includeEmojis !== false, + includeCallToAction: contentData.includeCallToAction !== false, + includeLocation: contentData.includeLocation || false, + + // ========== 플랫폼별 옵션 ========== + forInstagramStory: contentData.forInstagramStory || false, + forNaverBlogPost: contentData.forNaverBlogPost || false, + + // ========== AI 생성 옵션 ========== + alternativeTitleCount: contentData.alternativeTitleCount || 3, + alternativeHashtagSetCount: contentData.alternativeHashtagSetCount || 2, + preferredAiModel: contentData.preferredAiModel || 'gpt-4-turbo', + + // ========== 검증 플래그 ========== + validForPlatform: true, + validEventDates: true } - // null 값 제거 + // ✅ null/undefined 값 정리 Object.keys(requestData).forEach(key => { if (requestData[key] === null || requestData[key] === undefined) { delete requestData[key] } + // 빈 문자열도 제거 (Boolean과 Number 제외) + if (typeof requestData[key] === 'string' && requestData[key].trim() === '') { + delete requestData[key] + } }) - console.log('📝 [API] Java 백엔드용 SNS 요청 데이터:', requestData) + console.log('📝 [API] 완전한 SNS 요청 데이터:', requestData) - // ✅ request를 JSON 문자열로 FormData에 추가 + // ✅ FormData에 JSON 추가 formData.append('request', JSON.stringify(requestData)) - // ✅ 이미지 파일들을 FormData에 추가 (SNS는 선택적) - if (contentData.images && contentData.images.length > 0) { - // Base64 이미지를 Blob으로 변환하여 추가 + // ✅ 이미지 파일 처리 + let imageCount = 0 + if (contentData.images && Array.isArray(contentData.images) && contentData.images.length > 0) { for (let i = 0; i < contentData.images.length; i++) { const imageData = contentData.images[i] if (typeof imageData === 'string' && imageData.startsWith('data:image/')) { try { const blob = this.base64ToBlob(imageData) formData.append('files', blob, `image_${i}.jpg`) + imageCount++ } catch (error) { console.warn(`⚠️ 이미지 ${i} 변환 실패:`, error) } @@ -285,9 +319,19 @@ class ContentService { } } - console.log('📁 [API] FormData 구성 완료') + console.log(`📁 [API] FormData 구성 완료 (이미지 ${imageCount}개)`) - // ✅ multipart/form-data로 Java 백엔드 API 호출 + // ✅ 디버깅을 위한 FormData 내용 출력 + console.log('📋 [DEBUG] FormData 항목들:') + for (let [key, value] of formData.entries()) { + if (value instanceof Blob) { + console.log(` ${key}: Blob (${value.size} bytes, ${value.type})`) + } else { + console.log(` ${key}:`, value) + } + } + + // ✅ API 호출 const response = await contentApi.post('/sns/generate', formData, { timeout: 30000, headers: { @@ -297,13 +341,19 @@ class ContentService { console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data) - // ✅ Java 백엔드 ApiResponse 구조에 맞춰 처리 - if (response.data && response.data.success && response.data.data) { + // ✅ 응답 처리 + if (response.data?.success && response.data?.data) { return formatSuccessResponse({ - content: response.data.data.content, - hashtags: response.data.data.hashtags || [] + content: response.data.data.content || '', + hashtags: response.data.data.hashtags || [], + contentId: response.data.data.contentId, + platform: response.data.data.platform, + title: response.data.data.title, + alternativeTitles: response.data.data.alternativeTitles || [], + alternativeHashtagSets: response.data.data.alternativeHashtagSets || [] }, 'SNS 게시물이 생성되었습니다.') - } else if (response.data && response.data.status === 200 && response.data.data) { + } else if (response.data?.data?.content) { + // success 필드가 없는 경우도 처리 return formatSuccessResponse({ content: response.data.data.content, hashtags: response.data.data.hashtags || [] @@ -315,18 +365,32 @@ class ContentService { } 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 + // ✅ 상세한 에러 로깅 + if (error.response) { + console.error('❌ [DEBUG] HTTP Status:', error.response.status) + console.error('❌ [DEBUG] Response Headers:', error.response.headers) + console.error('❌ [DEBUG] Response Data:', error.response.data) + + 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 + } } - } else if (error.response?.status === 500) { + } else if (error.request) { + console.error('❌ [DEBUG] Request timeout or network error') return { success: false, - message: 'AI 서비스에서 콘텐츠 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', - error: error.response.data + message: '서버에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.', + error: 'NETWORK_ERROR' } } @@ -412,7 +476,7 @@ class ContentService { // ✅ multipart/form-data로 Java 백엔드 API 호출 const response = await contentApi.post('/poster/generate', formData, { - timeout: 60000, + timeout: 0, headers: { 'Content-Type': 'multipart/form-data' } @@ -545,67 +609,95 @@ class ContentService { * @returns {Promise} 저장 결과 */ async saveSnsContent(saveData) { - try { - 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.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) + try { + const requestData = {} + + // ❌ contentId 제거 (백엔드 DTO에 없음) + // if (saveData.contentId) requestData.contentId = saveData.contentId + + // ✅ 필수 필드들 + if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId + + // ✅ contentType 필수 필드 추가 - enum 값에 맞게 + requestData.contentType = 'SNS' // 첫 번째 enum 버전에 맞춤 + + // ✅ platform 필수 필드 보장 + if (saveData.platform) { + requestData.platform = saveData.platform + } else { + requestData.platform = 'INSTAGRAM' // 기본값 } + + // 선택적 필드들 + 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 + + console.log('📤 [API] SNS 저장 요청 데이터:', requestData) + + const response = await contentApi.post('/sns/save', requestData) + return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.') + } catch (error) { + console.error('❌ [API] SNS 저장 실패:', error) + return handleApiError(error) } +} /** - * 포스터 저장 (CON-015: 포스터 저장) + * 포스터 저장 (CON-015: 포스터 저장) - 수정된 버전 * @param {Object} saveData - 저장할 포스터 데이터 * @returns {Promise} 저장 결과 */ async savePoster(saveData) { - try { - 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.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) - } + try { + const requestData = {} + + // ❌ contentId 제거 (백엔드 DTO에 없음) + // 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 + + console.log('📤 [API] 포스터 저장 요청 데이터:', requestData) + + const response = await contentApi.post('/poster/save', requestData) + return formatSuccessResponse(response.data.data, '포스터가 저장되었습니다.') + } catch (error) { + console.error('❌ [API] 포스터 저장 실패:', error) + return handleApiError(error) } +} /** * ✅ 콘텐츠 저장 (통합) @@ -613,12 +705,24 @@ class ContentService { * @returns {Promise} 저장 결과 */ async saveContent(saveData) { - if (saveData.contentType === 'poster' || saveData.type === 'poster') { - return await this.savePoster(saveData) - } else { + try { + console.log('💾 [API] 콘텐츠 저장 요청:', saveData) + + // ✅ 콘텐츠 타입에 따라 다른 API 호출 + if (saveData.contentType === 'SNS' || saveData.platform) { + // SNS 콘텐츠 저장 + console.log('📱 [API] SNS 콘텐츠 저장 API 호출') return await this.saveSnsContent(saveData) + } else { + // 포스터 콘텐츠 저장 + console.log('🖼️ [API] 포스터 콘텐츠 저장 API 호출') + return await this.savePoster(saveData) } + } catch (error) { + console.error('❌ [API] 콘텐츠 저장 실패:', error) + return handleApiError(error) } +} /** * ✅ 진행 중인 콘텐츠 조회 (첫 번째 코드에서 추가) diff --git a/src/store/content.js b/src/store/content.js index a9be85a..ca59317 100644 --- a/src/store/content.js +++ b/src/store/content.js @@ -277,6 +277,146 @@ export const useContentStore = defineStore('content', () => { } } + /** + * 포스터 저장 - 수정된 버전 + */ + 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 구조에 맞게 데이터 변환 (contentId 처리 개선) + const requestData = { + // ✅ contentId 처리: 값이 있으면 사용, 없으면 임시 ID 생성 + contentId: saveData.contentId || Date.now(), + storeId: storeId, + title: saveData.title || '', + + // ✅ content 필드에 실제 값 보장 (null이면 안됨) + content: saveData.content || saveData.title || '포스터 콘텐츠', + + // ✅ images 배열이 비어있지 않도록 보장 + images: Array.isArray(saveData.images) ? saveData.images.filter(img => img) : [], + + status: saveData.status || 'PUBLISHED', + category: saveData.category || '이벤트', + requirement: saveData.requirement || '', + toneAndManner: saveData.toneAndManner || '친근함', + emotionIntensity: saveData.emotionIntensity || '보통', + eventName: saveData.eventName || '', + startDate: saveData.startDate, + endDate: saveData.endDate, + photoStyle: saveData.photoStyle || '밝고 화사한' + } + + // ✅ 필수 필드 검증 + if (!requestData.title) { + throw new Error('제목은 필수입니다.') + } + if (!requestData.images || requestData.images.length === 0) { + throw new Error('이미지는 필수입니다.') + } + + console.log('📝 [STORE] 최종 저장 요청 데이터:', { + ...requestData, + images: `${requestData.images.length}개 이미지` + }) + + 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 + } + } + + /** + * 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 || Date.now(), // 임시 ID 생성 + 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 || '보통', + eventName: saveData.eventName || '', + startDate: saveData.startDate, + endDate: saveData.endDate + } + + 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 + } + } + /** * fetchContentList를 실제 API 호출로 수정 (기존 호환성 유지) */ @@ -343,7 +483,7 @@ export const useContentStore = defineStore('content', () => { } } - // ===== 콘텐츠 관리 기능들 (첫 번째 코드에서 추가) ===== + // ===== 콘텐츠 관리 기능들 ===== /** * 콘텐츠 수정 @@ -674,6 +814,8 @@ export const useContentStore = defineStore('content', () => { loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함) generateContent, saveContent, + savePoster, // 포스터 전용 저장 + saveSnsContent, // SNS 콘텐츠 전용 저장 fetchContentList, // 기존 호환성 유지 fetchOngoingContents, fetchContentDetail, diff --git a/src/views/ContentCreationView.vue b/src/views/ContentCreationView.vue index df5294f..4368574 100644 --- a/src/views/ContentCreationView.vue +++ b/src/views/ContentCreationView.vue @@ -207,7 +207,7 @@ { if (type === 'poster') { @@ -820,51 +807,39 @@ const promotionEndDateRules = [ } ] -// ✅ 수정: 이미지 URL 유효성 검사 함수 - 더 관대하게 수정 -const getValidImageUrl = (imageUrl) => { - console.log('🖼️ 이미지 URL 검증:', imageUrl, typeof imageUrl) - - if (!imageUrl || typeof imageUrl !== 'string') { - console.log('❌ 이미지 URL이 문자열이 아님') - return null +// ✅ Computed 속성들 +const formValid = computed(() => { + // 기본 필수 필드 검증 + if (!formData.value.title || !formData.value.targetType) { + return false } - // 조건을 더 관대하게 수정 - 최소 길이만 체크 - if (imageUrl.length < 10) { - console.log('❌ 이미지 URL이 너무 짧음:', imageUrl.length) - return null + // SNS 타입인 경우 플랫폼 필수 + if (selectedType.value === 'sns' && !formData.value.platform) { + return false } - // 유효한 이미지 URL 형식 체크 (조건 완화) - if (imageUrl.startsWith('http') || - imageUrl.startsWith('data:image/') || - imageUrl.startsWith('blob:') || - imageUrl.startsWith('//') || - imageUrl.includes('blob.core.windows.net')) { // Azure Blob Storage 추가 - - console.log('✅ 유효한 이미지 URL:', imageUrl.substring(0, 50) + '...') - return imageUrl + // 이벤트 타입인 경우 추가 검증 + if (formData.value.targetType === 'event') { + if (!formData.value.eventName || !formData.value.startDate || !formData.value.endDate) { + return false + } } - console.log('❌ 유효하지 않은 이미지 URL 형식') - return null -} - -// ✅ 이미지 미리보기 함수 -const previewImage = (imageUrl, title) => { - if (!imageUrl) return + // 포스터 타입인 경우 추가 검증 + if (selectedType.value === 'poster') { + if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) { + return false + } + // 포스터는 이미지 필수 + if (!previewImages.value || previewImages.value.length === 0) { + return false + } + } - // 간단히 새 탭에서 이미지 열기 - window.open(imageUrl, '_blank') -} + return true +}) -// ✅ 추가: 이미지 에러 핸들링 함수 -const handleImageError = (event) => { - console.error('❌ 이미지 로딩 실패:', event.target?.src) - // 에러 시 플레이스홀더 표시를 위해 특별한 처리 필요 없음 (v-img의 error slot이 처리) -} - -// 수정: canGenerate computed 추가 const canGenerate = computed(() => { try { // 기본 조건들 확인 @@ -894,7 +869,6 @@ const canGenerate = computed(() => { } }) -// Computed const currentVersion = computed(() => { return generatedVersions.value[selectedVersion.value] || null }) @@ -905,7 +879,6 @@ const selectContentType = (type) => { console.log(`${type} 타입 선택됨`) } -// 수정: handleFileUpload 함수 - 중복 등록 방지 const handleFileUpload = (files) => { console.log('📁 파일 업로드 이벤트:', files) @@ -979,135 +952,159 @@ const removeImage = (index) => { } } -// ✅ 수정: generateContent 함수 - Java 백엔드에 맞게 데이터 구성 const generateContent = async () => { - if (!canGenerate.value || remainingGenerations.value <= 0) { - console.log('⚠️ 생성 조건을 만족하지 않음') + if (!formValid.value) { + appStore.showSnackbar('모든 필수 항목을 입력해주세요.', 'warning') return } - // 최대 3개 버전 체크 - if (generatedVersions.value.length >= 3) { - appStore.showSnackbar('최대 3개의 버전까지만 생성할 수 있습니다.', 'warning') + if (remainingGenerations.value <= 0) { + appStore.showSnackbar('생성 가능 횟수를 모두 사용했습니다.', 'warning') return } + isGenerating.value = true + try { - console.log('🎯 콘텐츠 생성 시작') + console.log('🚀 [UI] 콘텐츠 생성 시작') + console.log('📋 [UI] 폼 데이터:', formData.value) + console.log('📁 [UI] 이미지 데이터:', previewImages.value) - // ✅ 콘텐츠 타입에 따른 데이터 구성 분기 - let contentData + // ✅ 매장 ID 가져오기 + let storeId = 1 // 기본값 + try { + // localStorage에서 매장 정보 조회 시도 + const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}') + const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') + + if (storeInfo.storeId) { + storeId = storeInfo.storeId + } else if (userInfo.storeId) { + storeId = userInfo.storeId + } else { + console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId) + } + } catch (error) { + console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId) + } + + console.log('🏪 [UI] 사용할 매장 ID:', storeId) + + // ✅ Base64 이미지 URL 추출 + const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || [] + console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls) + + // ✅ 포스터 타입의 경우 이미지 필수 검증 + if (selectedType.value === 'poster' && imageUrls.length === 0) { + throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.') + } + + // ✅ 콘텐츠 생성 데이터 구성 + const contentData = { + title: formData.value.title, + platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'), + contentType: selectedType.value, + type: selectedType.value, + category: getCategory(formData.value.targetType), + requirement: formData.value.requirements || `${formData.value.title}에 대한 ${selectedType.value === 'poster' ? '포스터' : 'SNS 게시물'}를 만들어주세요`, + targetType: formData.value.targetType, + targetAudience: formData.value.targetType, + eventName: formData.value.eventName, + eventDate: formData.value.eventDate, + startDate: formData.value.startDate, + endDate: formData.value.endDate, + toneAndManner: formData.value.toneAndManner || '친근함', + emotionIntensity: formData.value.emotionIntensity || '보통', + images: imageUrls, // ✅ Base64 이미지 URL 배열 + storeId: storeId // ✅ 매장 ID 추가 + } + + // ✅ 포스터 전용 필드 추가 if (selectedType.value === 'poster') { - // ✅ Java 백엔드 PosterContentCreateRequest에 맞게 데이터 구성 - contentData = { - type: selectedType.value, - contentType: selectedType.value, - - // ✅ Java 백엔드 필수 필드들 (PosterContentCreateRequest 기준) - storeId: 1, - title: formData.value.title, - targetAudience: convertTargetAudienceToKorean(formData.value.targetType), - promotionStartDate: formData.value.promotionStartDate, - promotionEndDate: formData.value.promotionEndDate, - images: previewImages.value.map(img => img.url), - - // ✅ 선택적 필드들 (Java DTO에 맞춤) - menuName: formData.value.targetType === 'menu' ? formData.value.title : null, - eventName: formData.value.targetType === 'event' ? formData.value.eventName : null, - imageStyle: aiOptions.value.imageStyle || '모던', - category: getJavaCategory(formData.value.targetType), - requirement: formData.value.requirements || `${formData.value.title}에 대한 포스터를 만들어주세요`, - startDate: convertDateTimeToDateStrict(formData.value.startDate), - endDate: convertDateTimeToDateStrict(formData.value.endDate), - photoStyle: aiOptions.value.photoStyle || '밝고 화사한' - } - } else { - // ✅ Java 백엔드 SnsContentCreateRequest에 맞게 데이터 구성 - contentData = { - type: selectedType.value, - contentType: selectedType.value, - - // ✅ Java 백엔드 필수 필드들 (SnsContentCreateRequest 기준) - storeId: 1, - storeName: '샘플 매장', - storeType: '음식점', - platform: formData.value.platform, - title: formData.value.title, - category: getJavaCategory(formData.value.targetType), - requirement: formData.value.requirements || `${formData.value.title}에 대한 SNS 게시물을 만들어주세요`, - target: convertTargetAudienceToKorean(formData.value.targetType), - images: previewImages.value.map(img => img.url), - - // ✅ 선택적 필드들 - eventName: formData.value.targetType === 'event' ? formData.value.eventName : null, - startDate: convertDateTimeToDateStrict(formData.value.startDate), - endDate: convertDateTimeToDateStrict(formData.value.endDate) - } + contentData.promotionStartDate = formData.value.promotionStartDate + contentData.promotionEndDate = formData.value.promotionEndDate + contentData.imageStyle = formData.value.imageStyle || '모던' + contentData.promotionType = formData.value.promotionType + contentData.photoStyle = formData.value.photoStyle || '밝고 화사한' } - // ✅ undefined 값들 제거 (Java에서 오류 방지) - Object.keys(contentData).forEach(key => { - if (contentData[key] === undefined) { - delete contentData[key] - } - }) + console.log('📤 [UI] 생성 요청 데이터:', contentData) - console.log('🎯 [GENERATE] Java 백엔드용 데이터:', contentData) - - // ✅ 필수 필드 재검증 - if (!contentData.title) { - throw new Error('제목은 필수입니다.') + // ✅ contentData 무결성 체크 + if (!contentData || typeof contentData !== 'object') { + throw new Error('콘텐츠 데이터 구성에 실패했습니다.') } - if (selectedType.value === 'poster') { - if (!contentData.targetAudience) { - throw new Error('홍보 대상은 필수입니다.') - } - if (!contentData.promotionStartDate || !contentData.promotionEndDate) { - throw new Error('홍보 기간은 필수입니다.') - } - if (!contentData.images || contentData.images.length === 0) { - throw new Error('포스터 생성을 위해서는 이미지가 필요합니다.') - } - } else { - if (!contentData.platform) { - throw new Error('플랫폼은 필수입니다.') - } + if (!Array.isArray(contentData.images)) { + console.error('❌ [UI] contentData.images가 배열이 아님!') + contentData.images = [] } - // AI 콘텐츠 생성 - store.generateContent에 단일 파라미터로 전달 + // ✅ Store 호출 + console.log('🚀 [UI] contentStore.generateContent 호출') const generated = await contentStore.generateContent(contentData) - - console.log('🎯 [GENERATE] AI 생성 응답:', generated) - - if (generated && generated.success) { - const newContent = { - id: Date.now() + Math.random(), - ...contentData, - // 프론트엔드 표시용 원본 데이터도 보존 - targetType: formData.value.targetType, - platform: selectedType.value === 'sns' ? formData.value.platform : 'poster', - content: generated.content || generated.data?.content || '생성된 콘텐츠 내용', - hashtags: generated.hashtags || generated.data?.hashtags || [], - createdAt: new Date(), - status: 'draft', - // ✅ 포스터인 경우 posterImage 필드 추가 - posterImage: selectedType.value === 'poster' ? (generated.posterImage || generated.data?.posterImage || generated.content) : null - } - - generatedVersions.value.push(newContent) - selectedVersion.value = generatedVersions.value.length - 1 - remainingGenerations.value-- - - console.log('✅ [GENERATE] AI 콘텐츠 생성 성공:', newContent) - appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success') - } else { - throw new Error(generated?.error || '콘텐츠 생성에 실패했습니다.') + + if (!generated || !generated.success) { + throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.') } + + // ✅ 포스터 생성 결과 처리 개선 + let finalContent = '' + let posterImageUrl = '' + + if (selectedType.value === 'poster') { + // 포스터의 경우 generated.data에서 이미지 URL 추출 + posterImageUrl = generated.data?.posterImage || generated.data?.content || generated.content || '' + finalContent = posterImageUrl // content 필드에 이미지 URL 저장 + + console.log('🖼️ [UI] 포스터 이미지 URL:', posterImageUrl) + } else { + // SNS의 경우 기존 로직 유지 + finalContent = generated.content || generated.data?.content || '' + + // SNS용 이미지 추가 + if (contentData.images && contentData.images.length > 0) { + const imageHtml = contentData.images.map(imageUrl => + `
+ +
` + ).join('') + + if (isHtmlContent(finalContent)) { + finalContent = imageHtml + finalContent + } else { + finalContent = imageHtml + `
${finalContent.replace(/\n/g, '
')}
` + } + } + } + + // ✅ 생성된 콘텐츠 객체에 이미지 정보 포함 + const newContent = { + id: Date.now() + Math.random(), + ...contentData, + content: finalContent, + posterImage: posterImageUrl, // 포스터 이미지 URL 별도 저장 + hashtags: generated.hashtags || generated.data?.hashtags || [], + createdAt: new Date(), + status: 'draft', + uploadedImages: previewImages.value || [], // ✅ 업로드된 이미지 정보 보존 + images: imageUrls, // ✅ Base64 URL 보존 + platform: contentData.platform || 'POSTER' + } + + generatedVersions.value.push(newContent) + selectedVersion.value = generatedVersions.value.length - 1 + remainingGenerations.value-- + + appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success') + } catch (error) { - console.error('❌ [GENERATE] 콘텐츠 생성 실패:', error) - appStore.showSnackbar(`콘텐츠 생성 중 오류가 발생했습니다: ${error.message}`, 'error') + console.error('❌ [UI] 콘텐츠 생성 실패:', error) + console.error('❌ [UI] 에러 스택:', error.stack) + appStore.showSnackbar(error.message || '콘텐츠 생성 중 오류가 발생했습니다.', 'error') + } finally { + isGenerating.value = false } } @@ -1116,7 +1113,9 @@ const getCategory = (targetType) => { 'new_menu': '메뉴소개', 'discount': '이벤트', 'store': '인테리어', - 'event': '이벤트' + 'event': '이벤트', + 'menu': '메뉴소개', + 'service': '서비스' } return mapping[targetType] || '기타' } @@ -1132,37 +1131,151 @@ const saveVersion = async (index) => { try { const version = generatedVersions.value[index] - // contentStore.saveContent에 단일 파라미터로 전달 - const saveData = { - type: version.type || version.contentType, - contentType: version.contentType || version.type, - title: version.title, - content: version.content, - hashtags: version.hashtags, - platform: version.platform, - category: getCategory(version.targetType), - eventName: version.eventName, - eventDate: version.eventDate, - status: 'PUBLISHED', - storeId: version.storeId + console.log('💾 [UI] 저장할 버전 데이터:', version) + + // ✅ 매장 ID 가져오기 + let storeId = 1 // 기본값 + + try { + const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}') + const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') + + if (storeInfo.storeId) { + storeId = storeInfo.storeId + } else if (userInfo.storeId) { + storeId = userInfo.storeId + } else { + console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId) + } + } catch (error) { + console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId) } - const result = await contentStore.saveContent(saveData) + console.log('🏪 [UI] 사용할 매장 ID:', storeId) - if (result.success) { - version.status = 'published' - version.publishedAt = new Date() + // ✅ 이미지 데이터 준비 + let imageUrls = [] + + // 포스터의 경우 생성된 포스터 이미지 URL과 업로드된 이미지들을 포함 + if (selectedType.value === 'poster') { + // 1. 생성된 포스터 이미지 URL 추가 + if (version.posterImage) { + imageUrls.push(version.posterImage) + console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage) + } - appStore.showSnackbar(`버전 ${index + 1}이 성공적으로 저장되었습니다!`, 'success') + // 2. previewImages에서 원본 이미지 URL 추가 + if (previewImages.value && previewImages.value.length > 0) { + const originalImages = previewImages.value.map(img => img.url).filter(url => url) + imageUrls = [...imageUrls, ...originalImages] + console.log('💾 [UI] 원본 이미지들:', originalImages) + } - setTimeout(() => { - if (confirm('저장된 콘텐츠를 확인하시겠습니까?')) { - router.push('/content') - } - }, 1000) + // 3. version에 저장된 이미지도 확인 + if (version.uploadedImages && version.uploadedImages.length > 0) { + const versionImages = version.uploadedImages.map(img => img.url).filter(url => url) + imageUrls = [...imageUrls, ...versionImages] + } + + // 4. version.images도 확인 + if (version.images && Array.isArray(version.images) && version.images.length > 0) { + imageUrls = [...imageUrls, ...version.images] + } + + // 중복 제거 + imageUrls = [...new Set(imageUrls)] + + console.log('💾 [UI] 포스터 최종 이미지 URL들:', imageUrls) + + // 이미지가 없으면 에러 + if (!imageUrls || imageUrls.length === 0) { + throw new Error('포스터 저장을 위해 최소 1개의 이미지가 필요합니다.') + } } else { - throw new Error(result.error || '저장에 실패했습니다.') + // SNS의 경우 선택적으로 이미지 포함 + if (previewImages.value && previewImages.value.length > 0) { + imageUrls = previewImages.value.map(img => img.url).filter(url => url) + } + if (version.images && Array.isArray(version.images)) { + imageUrls = [...new Set([...imageUrls, ...version.images])] + } } + + console.log('💾 [UI] 최종 이미지 URL들:', imageUrls) + + // ✅ 저장 데이터 구성 - 타입에 따라 다르게 처리 + let saveData + + if (selectedType.value === 'poster') { + // 포스터용 데이터 구성 (PosterContentSaveRequest에 맞춤) + saveData = { + // 매장 ID + storeId: storeId, + + // 기본 콘텐츠 정보 - 포스터는 content에 이미지 URL 저장 + title: version.title, + content: version.posterImage || version.content, // 포스터 이미지 URL을 content에 저장 + images: imageUrls, // 모든 관련 이미지들 + + // 분류 정보 + category: getCategory(version.targetType || formData.value.targetType), + requirement: formData.value.requirements || `${version.title}에 대한 포스터를 만들어주세요`, + + // 이벤트 정보 + eventName: version.eventName || formData.value.eventName, + startDate: formData.value.startDate, + endDate: formData.value.endDate, + + // 스타일 정보 + photoStyle: formData.value.photoStyle || '밝고 화사한' + } + } else { + // SNS용 데이터 구성 (SnsContentSaveRequest에 맞춤) + saveData = { + // 매장 ID + storeId: storeId, + + // 필수 필드들 + contentType: 'SNS', + platform: version.platform || formData.value.platform || 'INSTAGRAM', + + // 기본 콘텐츠 정보 + title: version.title, + content: version.content, + hashtags: version.hashtags || [], + images: imageUrls, + + // 분류 정보 + category: getCategory(version.targetType || formData.value.targetType), + requirement: formData.value.requirements || `${version.title}에 대한 SNS 게시물을 만들어주세요`, + toneAndManner: formData.value.toneAndManner || '친근함', + emotionIntensity: formData.value.emotionIntensity || '보통', + + // 이벤트 정보 + eventName: version.eventName || formData.value.eventName, + startDate: formData.value.startDate, + endDate: formData.value.endDate, + + // 상태 정보 + status: 'PUBLISHED' + } + } + + console.log('💾 [UI] 최종 저장 데이터:', saveData) + + // ✅ 저장 실행 + await contentStore.saveContent(saveData) + + version.status = 'published' + version.publishedAt = new Date() + + appStore.showSnackbar(`버전 ${index + 1}이 성공적으로 저장되었습니다!`, 'success') + + setTimeout(() => { + if (confirm('저장된 콘텐츠를 확인하시겠습니까?')) { + router.push('/content') + } + }, 1000) } catch (error) { console.error('❌ 콘텐츠 저장 실패:', error) appStore.showSnackbar(error.message || '콘텐츠 저장 중 오류가 발생했습니다.', 'error') @@ -1216,7 +1329,8 @@ const getPlatformIcon = (platform) => { 'INSTAGRAM': 'mdi-instagram', 'NAVER_BLOG': 'mdi-web', 'FACEBOOK': 'mdi-facebook', - 'KAKAO_STORY': 'mdi-chat' + 'KAKAO_STORY': 'mdi-chat', + 'POSTER': 'mdi-image' } return icons[platform] || 'mdi-web' } @@ -1230,7 +1344,8 @@ const getPlatformColor = (platform) => { 'INSTAGRAM': 'pink', 'NAVER_BLOG': 'green', 'FACEBOOK': 'blue', - 'KAKAO_STORY': 'amber' + 'KAKAO_STORY': 'amber', + 'POSTER': 'orange' } return colors[platform] || 'grey' } @@ -1244,90 +1359,12 @@ const getPlatformLabel = (platform) => { 'INSTAGRAM': '인스타그램', 'NAVER_BLOG': '네이버 블로그', 'FACEBOOK': '페이스북', - 'KAKAO_STORY': '카카오스토리' + 'KAKAO_STORY': '카카오스토리', + 'POSTER': '포스터' } return labels[platform] || platform } -// ✅ Java 백엔드 형식 변환 함수들 -const convertTargetAudienceToKorean = (targetType) => { - const mapping = { - 'menu': '메뉴', - 'store': '매장', - 'event': '이벤트', - 'service': '서비스', - 'discount': '할인혜택' - } - return mapping[targetType] || '기타' -} - -// ✅ Java 백엔드용 카테고리 변환 (정확한 값 사용) -const getJavaCategory = (targetType) => { - const mapping = { - 'menu': '메뉴소개', - 'store': '매장홍보', - 'event': '이벤트', - 'service': '서비스', - 'discount': '이벤트' - } - return mapping[targetType] || '이벤트' -} - -const convertCategoryToKorean = (category) => { - const mapping = { - '음식': '이벤트', - '매장': '이벤트', - '이벤트': '이벤트', - '기타': '이벤트' - } - return mapping[category] || '이벤트' -} - -// ✅ 날짜를 YYYY-MM-DD 형식으로 엄격하게 변환 -const convertDateTimeToDateStrict = (dateTimeString) => { - if (!dateTimeString) return undefined // null 대신 undefined 반환 - - try { - let dateStr = dateTimeString - - // "2025-06-19T09:58" -> "2025-06-19" 형식으로 변환 - if (dateTimeString.includes('T')) { - dateStr = dateTimeString.split('T')[0] - } - - // YYYY-MM-DD 형식 검증 - const dateRegex = /^\d{4}-\d{2}-\d{2}$/ - if (!dateRegex.test(dateStr)) { - console.warn('⚠️ 잘못된 날짜 형식:', dateTimeString) - return undefined - } - - // 유효한 날짜인지 확인 - const date = new Date(dateStr) - if (isNaN(date.getTime())) { - console.warn('⚠️ 유효하지 않은 날짜:', dateStr) - return undefined - } - - return dateStr - } catch (error) { - console.error('❌ 날짜 변환 오류:', error, dateTimeString) - return undefined - } -} - -const convertDateTimeToDate = (dateTimeString) => { - if (!dateTimeString) return null - - // "2025-06-19T09:58" -> "2025-06-19" 형식으로 변환 - if (dateTimeString.includes('T')) { - return dateTimeString.split('T')[0] - } - - // 이미 YYYY-MM-DD 형식인 경우 그대로 반환 - return dateTimeString -} - const getStatusColor = (status) => { const colors = { 'draft': 'grey', @@ -1396,6 +1433,15 @@ const truncateHtmlContent = (html, maxLength) => { return `
${truncateText(textContent, maxLength)}
` } +const previewImage = (imageUrl, title) => { + if (!imageUrl) return + window.open(imageUrl, '_blank') +} + +const handleImageError = (event) => { + console.error('❌ 이미지 로딩 실패:', event.target?.src) +} + // 라이프사이클 onMounted(() => { console.log('📱 콘텐츠 생성 페이지 로드됨') @@ -1481,4 +1527,5 @@ onMounted(() => { background: linear-gradient(transparent, white); pointer-events: none; } - \ No newline at end of file + + \ No newline at end of file diff --git a/src/views/ContentManagementView.vue b/src/views/ContentManagementView.vue index b6af897..1d32931 100644 --- a/src/views/ContentManagementView.vue +++ b/src/views/ContentManagementView.vue @@ -152,8 +152,22 @@ {{ getStatusText(content.status) }} + +
- {{ content.content ? content.content.substring(0, 100) + '...' : '' }} +
+
+ mdi-image + 포스터 이미지 생성됨 +
+
+ {{ content.content ? content.content.substring(0, 100) + '...' : '포스터 콘텐츠' }} +
+
+ +
+ {{ content.content ? getPlainTextPreview(content.content) : '' }} +
@@ -272,9 +286,61 @@ +
내용
-
{{ selectedContent.content }}
+ + +
+ + + + + + +
+ mdi-image-off + 포스터 이미지가 없습니다 + + URL: {{ selectedContent.content }} + +
+
+ + +
+ +
+
+ +
+ {{ selectedContent.content }} +
+
@@ -664,7 +730,78 @@ const deleteSelectedItems = async () => { } } -// 유틸리티 함수들 +// ✅ 유틸리티 함수들 추가 +/** + * URL이 이미지 URL인지 확인 + */ +const isImageUrl = (url) => { + if (!url || typeof url !== 'string') return false + + // 이미지 확장자 체크 + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'] + const lowerUrl = url.toLowerCase() + + // 확장자로 체크 + if (imageExtensions.some(ext => lowerUrl.includes(ext))) { + return true + } + + // Blob URL 체크 + if (url.startsWith('blob:') || url.startsWith('data:image/')) { + return true + } + + // Azure Blob Storage나 기타 클라우드 스토리지 URL 체크 + if (url.includes('blob.core.windows.net') || + url.includes('amazonaws.com') || + url.includes('googleusercontent.com') || + url.includes('cloudinary.com')) { + return true + } + + return false +} + +/** + * ✅ HTML 콘텐츠인지 확인 + */ +const isHtmlContent = (content) => { + if (!content || typeof content !== 'string') return false + + // HTML 태그가 포함되어 있는지 확인 + const htmlTagRegex = /<[^>]+>/g + return htmlTagRegex.test(content) +} + +/** + * ✅ HTML 태그를 제거하고 텍스트만 추출하여 미리보기용으로 반환 + */ +const getPlainTextPreview = (content) => { + if (!content) return '' + + // HTML 태그 제거 + const textContent = content.replace(/<[^>]*>/g, '').trim() + + // 100자로 제한하고 ... 추가 + return textContent.length > 100 ? textContent.substring(0, 100) + '...' : textContent +} + +/** + * 이미지 미리보기 + */ +const previewImage = (imageUrl, title) => { + if (!imageUrl) return + window.open(imageUrl, '_blank') +} + +/** + * 이미지 로딩 에러 처리 + */ +const handleImageError = (event) => { + console.error('❌ 이미지 로딩 실패:', event.target?.src) +} + +// 기존 유틸리티 함수들 const getStatusColor = (status) => { const statusColors = { 'DRAFT': 'orange', @@ -783,4 +920,61 @@ onMounted(() => { .cursor-pointer { cursor: pointer; } + +/* ✅ HTML 콘텐츠 스타일링 */ +:deep(.html-content) { + font-family: 'Noto Sans KR', Arial, sans-serif; + line-height: 1.6; + padding: 20px; + max-width: 600px; +} + +:deep(.html-content h1), +:deep(.html-content h2), +:deep(.html-content h3) { + margin: 0 0 10px 0; + font-weight: bold; +} + +:deep(.html-content h3) { + font-size: 18px; + color: #1976d2; +} + +:deep(.html-content p) { + margin: 0 0 10px 0; +} + +:deep(.html-content div[style*="background"]) { + border-radius: 10px; + padding: 15px; + margin: 10px 0; +} + +:deep(.html-content div[style*="border"]) { + border-radius: 8px; + padding: 20px; + margin: 10px 0; + border: 1px solid #e1e8ed; +} + +:deep(.html-content img) { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + margin: 20px 0; +} + +:deep(.html-content span[style*="#1DA1F2"]) { + color: #1976d2 !important; +} + +:deep(.html-content span[style*="#1EC800"]) { + color: #4caf50 !important; +} + +:deep(.html-content span[style*="#00B33C"]) { + color: #2e7d32 !important; +} \ No newline at end of file diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 280e3ba..6a18c62 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -252,12 +252,7 @@
-
- {{ aiRecommendation.emoji }} -

- {{ aiRecommendation.title }} -

-
+
diff --git a/src/views/PosterCreationView.vue b/src/views/PosterCreationView.vue index 63b1e0b..bc547f4 100644 --- a/src/views/PosterCreationView.vue +++ b/src/views/PosterCreationView.vue @@ -167,7 +167,7 @@ const generatePoster = async (formData) => { } /** - * 포스터 저장 + * 포스터 저장 - 수정된 버전 */ const savePoster = async () => { if (!generatedPoster.value) { @@ -181,12 +181,22 @@ const savePoster = async () => { loadingMessage.value = '포스터 저장 중...' loadingSubMessage.value = '잠시만 기다려주세요' + // ✅ 생성된 포스터의 실제 데이터를 사용하고 contentId 처리 개선 const result = await posterStore.savePoster({ - contentId: generatedPoster.value.contentId, + // ✅ contentId: 임시 ID 사용 (백엔드에서 @NotNull이므로) + contentId: generatedPoster.value.contentId || Date.now(), // 임시 ID 생성 storeId: authStore.currentStore?.id || 1, title: posterForm.value.title, - content: generatedPoster.value.content, - images: [generatedPoster.value.posterImage], + + // ✅ 생성된 포스터의 실제 콘텐츠 정보 사용 + content: generatedPoster.value.content || generatedPoster.value.description || posterForm.value.title, + + // ✅ 원본 이미지와 생성된 포스터 이미지 모두 포함 + images: [ + ...posterForm.value.images, // 원본 업로드 이미지들 + generatedPoster.value.posterImage || generatedPoster.value.imageUrl // 생성된 포스터 이미지 + ].filter(img => img), // null/undefined 제거 + status: 'PUBLISHED', category: posterForm.value.category, requirement: posterForm.value.requirement,