This commit is contained in:
SeoJHeasdw 2025-06-19 16:03:55 +09:00
parent 4c1381203f
commit 4a252efa72
6 changed files with 872 additions and 380 deletions

View File

@ -217,7 +217,7 @@ class ContentService {
} }
/** /**
* multipart/form-data 형식으로 수정된 SNS 콘텐츠 생성 * 완전한 SnsContentCreateRequest DTO에 맞춘 SNS 콘텐츠 생성
* @param {Object} contentData - 콘텐츠 생성 데이터 * @param {Object} contentData - 콘텐츠 생성 데이터
* @returns {Promise<Object>} 생성된 콘텐츠 * @returns {Promise<Object>} 생성된 콘텐츠
*/ */
@ -225,7 +225,7 @@ class ContentService {
try { try {
console.log('🤖 SNS 콘텐츠 생성 요청:', contentData) console.log('🤖 SNS 콘텐츠 생성 요청:', contentData)
// ✅ Java 백엔드 필수 필드 검증 (SnsContentCreateRequest 기준) // ✅ 필수 필드 검증
if (!contentData.storeId) { if (!contentData.storeId) {
throw new Error('매장 ID는 필수입니다.') throw new Error('매장 ID는 필수입니다.')
} }
@ -238,46 +238,80 @@ class ContentService {
throw new Error('콘텐츠 제목은 필수입니다.') throw new Error('콘텐츠 제목은 필수입니다.')
} }
// ✅ FormData 생성 (multipart/form-data) // ✅ FormData 생성
const formData = new FormData() const formData = new FormData()
// ✅ request JSON 부분 구성 (Java SnsContentCreateRequest DTO에 맞춤) // ✅ 완전한 SnsContentCreateRequest DTO에 맞춘 데이터 구성
const requestData = { const requestData = {
storeId: contentData.storeId || 1, // ========== 기본 정보 ==========
storeId: parseInt(contentData.storeId),
storeName: contentData.storeName || '샘플 매장', storeName: contentData.storeName || '샘플 매장',
storeType: contentData.storeType || '음식점', storeType: contentData.storeType || '음식점',
platform: this.normalizePlatform(contentData.platform), platform: this.normalizePlatform(contentData.platform),
title: contentData.title, title: String(contentData.title).trim(),
// ========== 콘텐츠 생성 조건 ==========
category: contentData.category || '메뉴소개', category: contentData.category || '메뉴소개',
requirement: contentData.requirement || contentData.requirements || `${contentData.title}에 대한 SNS 게시물을 만들어주세요`, requirement: contentData.requirement || contentData.requirements || `${contentData.title}에 대한 SNS 게시물을 만들어주세요`,
target: contentData.target || contentData.targetAudience || '일반고객', target: contentData.target || contentData.targetAudience || '일반고객',
contentType: contentData.contentType || 'sns', toneAndManner: contentData.toneAndManner || '친근함',
eventName: contentData.eventName || null, emotionIntensity: contentData.emotionIntensity || contentData.emotionalIntensity || '보통',
// ========== 이벤트 정보 ==========
eventName: contentData.eventName || '',
startDate: this.convertToJavaDate(contentData.startDate), 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 => { Object.keys(requestData).forEach(key => {
if (requestData[key] === null || requestData[key] === undefined) { if (requestData[key] === null || requestData[key] === undefined) {
delete requestData[key] 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.append('request', JSON.stringify(requestData))
// ✅ 이미지 파일들을 FormData에 추가 (SNS는 선택적) // ✅ 이미지 파일 처리
if (contentData.images && contentData.images.length > 0) { let imageCount = 0
// Base64 이미지를 Blob으로 변환하여 추가 if (contentData.images && Array.isArray(contentData.images) && contentData.images.length > 0) {
for (let i = 0; i < contentData.images.length; i++) { for (let i = 0; i < contentData.images.length; i++) {
const imageData = contentData.images[i] const imageData = contentData.images[i]
if (typeof imageData === 'string' && imageData.startsWith('data:image/')) { if (typeof imageData === 'string' && imageData.startsWith('data:image/')) {
try { try {
const blob = this.base64ToBlob(imageData) const blob = this.base64ToBlob(imageData)
formData.append('files', blob, `image_${i}.jpg`) formData.append('files', blob, `image_${i}.jpg`)
imageCount++
} catch (error) { } catch (error) {
console.warn(`⚠️ 이미지 ${i} 변환 실패:`, 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, { const response = await contentApi.post('/sns/generate', formData, {
timeout: 30000, timeout: 30000,
headers: { headers: {
@ -297,13 +341,19 @@ class ContentService {
console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data) 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({ return formatSuccessResponse({
content: response.data.data.content, content: response.data.data.content || '',
hashtags: response.data.data.hashtags || [] 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 게시물이 생성되었습니다.') }, 'SNS 게시물이 생성되었습니다.')
} else if (response.data && response.data.status === 200 && response.data.data) { } else if (response.data?.data?.content) {
// success 필드가 없는 경우도 처리
return formatSuccessResponse({ return formatSuccessResponse({
content: response.data.data.content, content: response.data.data.content,
hashtags: response.data.data.hashtags || [] hashtags: response.data.data.hashtags || []
@ -315,20 +365,34 @@ class ContentService {
} catch (error) { } catch (error) {
console.error('❌ [API] SNS 콘텐츠 생성 실패:', error) console.error('❌ [API] SNS 콘텐츠 생성 실패:', error)
if (error.response?.status === 400) { // ✅ 상세한 에러 로깅
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 || '요청 데이터가 잘못되었습니다.' const backendMessage = error.response.data?.message || '요청 데이터가 잘못되었습니다.'
return { return {
success: false, success: false,
message: backendMessage, message: `요청 검증 실패: ${backendMessage}`,
error: error.response.data error: error.response.data
} }
} else if (error.response?.status === 500) { } else if (error.response.status === 500) {
return { return {
success: false, success: false,
message: 'AI 서비스에서 콘텐츠 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', message: 'AI 서비스에서 콘텐츠 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
error: error.response.data error: error.response.data
} }
} }
} else if (error.request) {
console.error('❌ [DEBUG] Request timeout or network error')
return {
success: false,
message: '서버에 연결할 수 없습니다. 네트워크 연결을 확인해 주세요.',
error: 'NETWORK_ERROR'
}
}
return handleApiError(error) return handleApiError(error)
} }
@ -412,7 +476,7 @@ class ContentService {
// ✅ multipart/form-data로 Java 백엔드 API 호출 // ✅ multipart/form-data로 Java 백엔드 API 호출
const response = await contentApi.post('/poster/generate', formData, { const response = await contentApi.post('/poster/generate', formData, {
timeout: 60000, timeout: 0,
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }
@ -548,9 +612,23 @@ class ContentService {
try { try {
const requestData = {} const requestData = {}
if (saveData.contentId) requestData.contentId = saveData.contentId // ❌ contentId 제거 (백엔드 DTO에 없음)
// if (saveData.contentId) requestData.contentId = saveData.contentId
// ✅ 필수 필드들
if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId
if (saveData.platform) requestData.platform = saveData.platform
// ✅ 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.title) requestData.title = saveData.title
if (saveData.content) requestData.content = saveData.content if (saveData.content) requestData.content = saveData.content
if (saveData.hashtags) requestData.hashtags = saveData.hashtags if (saveData.hashtags) requestData.hashtags = saveData.hashtags
@ -560,21 +638,28 @@ class ContentService {
if (saveData.status) requestData.status = saveData.status if (saveData.status) requestData.status = saveData.status
if (saveData.category) requestData.category = saveData.category if (saveData.category) requestData.category = saveData.category
if (saveData.requirement) requestData.requirement = saveData.requirement 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.eventName) requestData.eventName = saveData.eventName
if (saveData.startDate) requestData.startDate = saveData.startDate if (saveData.startDate) requestData.startDate = saveData.startDate
if (saveData.endDate) requestData.endDate = saveData.endDate if (saveData.endDate) requestData.endDate = saveData.endDate
if (saveData.promotionalType) requestData.promotionalType = saveData.promotionalType if (saveData.promotionalType) requestData.promotionalType = saveData.promotionalType
if (saveData.eventDate) requestData.eventDate = saveData.eventDate if (saveData.eventDate) requestData.eventDate = saveData.eventDate
console.log('📤 [API] SNS 저장 요청 데이터:', requestData)
const response = await contentApi.post('/sns/save', requestData) const response = await contentApi.post('/sns/save', requestData)
return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.') return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.')
} catch (error) { } catch (error) {
console.error('❌ [API] SNS 저장 실패:', error)
return handleApiError(error) return handleApiError(error)
} }
} }
/** /**
* 포스터 저장 (CON-015: 포스터 저장) * 포스터 저장 (CON-015: 포스터 저장) - 수정된 버전
* @param {Object} saveData - 저장할 포스터 데이터 * @param {Object} saveData - 저장할 포스터 데이터
* @returns {Promise<Object>} 저장 결과 * @returns {Promise<Object>} 저장 결과
*/ */
@ -582,7 +667,9 @@ class ContentService {
try { try {
const requestData = {} const requestData = {}
if (saveData.contentId) requestData.contentId = saveData.contentId // ❌ contentId 제거 (백엔드 DTO에 없음)
// if (saveData.contentId) requestData.contentId = saveData.contentId
if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId if (saveData.storeId !== undefined) requestData.storeId = saveData.storeId
if (saveData.title) requestData.title = saveData.title if (saveData.title) requestData.title = saveData.title
if (saveData.content) requestData.content = saveData.content if (saveData.content) requestData.content = saveData.content
@ -590,6 +677,8 @@ class ContentService {
if (saveData.status) requestData.status = saveData.status if (saveData.status) requestData.status = saveData.status
if (saveData.category) requestData.category = saveData.category if (saveData.category) requestData.category = saveData.category
if (saveData.requirement) requestData.requirement = saveData.requirement 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.eventName) requestData.eventName = saveData.eventName
if (saveData.startDate) requestData.startDate = saveData.startDate if (saveData.startDate) requestData.startDate = saveData.startDate
if (saveData.endDate) requestData.endDate = saveData.endDate if (saveData.endDate) requestData.endDate = saveData.endDate
@ -600,9 +689,12 @@ class ContentService {
if (saveData.promotionStartDate) requestData.promotionStartDate = saveData.promotionStartDate if (saveData.promotionStartDate) requestData.promotionStartDate = saveData.promotionStartDate
if (saveData.promotionEndDate) requestData.promotionEndDate = saveData.promotionEndDate if (saveData.promotionEndDate) requestData.promotionEndDate = saveData.promotionEndDate
console.log('📤 [API] 포스터 저장 요청 데이터:', requestData)
const response = await contentApi.post('/poster/save', requestData) const response = await contentApi.post('/poster/save', requestData)
return formatSuccessResponse(response.data.data, '포스터가 저장되었습니다.') return formatSuccessResponse(response.data.data, '포스터가 저장되었습니다.')
} catch (error) { } catch (error) {
console.error('❌ [API] 포스터 저장 실패:', error)
return handleApiError(error) return handleApiError(error)
} }
} }
@ -613,10 +705,22 @@ class ContentService {
* @returns {Promise<Object>} 저장 결과 * @returns {Promise<Object>} 저장 결과
*/ */
async saveContent(saveData) { async saveContent(saveData) {
if (saveData.contentType === 'poster' || saveData.type === 'poster') { try {
return await this.savePoster(saveData) console.log('💾 [API] 콘텐츠 저장 요청:', saveData)
} else {
// ✅ 콘텐츠 타입에 따라 다른 API 호출
if (saveData.contentType === 'SNS' || saveData.platform) {
// SNS 콘텐츠 저장
console.log('📱 [API] SNS 콘텐츠 저장 API 호출')
return await this.saveSnsContent(saveData) return await this.saveSnsContent(saveData)
} else {
// 포스터 콘텐츠 저장
console.log('🖼️ [API] 포스터 콘텐츠 저장 API 호출')
return await this.savePoster(saveData)
}
} catch (error) {
console.error('❌ [API] 콘텐츠 저장 실패:', error)
return handleApiError(error)
} }
} }

View File

@ -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 호출로 수정 (기존 호환성 유지) * fetchContentList를 실제 API 호출로 수정 (기존 호환성 유지)
*/ */
@ -343,7 +483,7 @@ export const useContentStore = defineStore('content', () => {
} }
} }
// ===== 콘텐츠 관리 기능들 (첫 번째 코드에서 추가) ===== // ===== 콘텐츠 관리 기능들 =====
/** /**
* 콘텐츠 수정 * 콘텐츠 수정
@ -674,6 +814,8 @@ export const useContentStore = defineStore('content', () => {
loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함) loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함)
generateContent, generateContent,
saveContent, saveContent,
savePoster, // 포스터 전용 저장
saveSnsContent, // SNS 콘텐츠 전용 저장
fetchContentList, // 기존 호환성 유지 fetchContentList, // 기존 호환성 유지
fetchOngoingContents, fetchOngoingContents,
fetchContentDetail, fetchContentDetail,

View File

@ -207,7 +207,7 @@
<!-- 요구사항 --> <!-- 요구사항 -->
<v-textarea <v-textarea
v-model="formData.requirements" v-model="formData.requirements"
label="구체적인 요구사항 (선택사항)" label="구체적인 요구사항"
variant="outlined" variant="outlined"
rows="3" rows="3"
density="compact" density="compact"
@ -664,19 +664,19 @@ const router = useRouter()
const contentStore = useContentStore() const contentStore = useContentStore()
const appStore = useAppStore() const appStore = useAppStore()
// // - isGenerating
const selectedType = ref('sns') const selectedType = ref('sns')
const formValid = ref(false)
const uploadedFiles = ref([]) const uploadedFiles = ref([])
const previewImages = ref([]) const previewImages = ref([])
const isPublishing = ref(false) const isPublishing = ref(false)
const isGenerating = ref(false) //
const publishingIndex = ref(-1) const publishingIndex = ref(-1)
const showDetailDialog = ref(false) const showDetailDialog = ref(false)
const selectedVersion = ref(0) const selectedVersion = ref(0)
const generatedVersions = ref([]) const generatedVersions = ref([])
const remainingGenerations = ref(3) const remainingGenerations = ref(3)
// - //
const formData = ref({ const formData = ref({
title: '', title: '',
platform: '', platform: '',
@ -691,9 +691,14 @@ const formData = ref({
promotionStartDate: '', promotionStartDate: '',
promotionEndDate: '', promotionEndDate: '',
requirements: '', requirements: '',
toneAndManner: '친근함',
emotionIntensity: '보통',
imageStyle: '모던',
promotionType: '할인 정보',
photoStyle: '밝고 화사한'
}) })
// AI - // AI
const aiOptions = ref({ const aiOptions = ref({
toneAndManner: 'friendly', toneAndManner: 'friendly',
promotion: 'general', promotion: 'general',
@ -734,14 +739,6 @@ const targetTypes = [
{ title: '이벤트', value: 'event' }, { title: '이벤트', value: 'event' },
] ]
//
const categoryOptions = [
{ title: '음식', value: '음식' },
{ title: '매장', value: '매장' },
{ title: '이벤트', value: '이벤트' },
{ title: '기타', value: '기타' }
]
// //
const targetAgeOptions = [ const targetAgeOptions = [
{ title: '10대', value: '10대' }, { title: '10대', value: '10대' },
@ -752,16 +749,6 @@ const targetAgeOptions = [
{ title: '60대 이상', value: '60대 이상' } { title: '60대 이상', value: '60대 이상' }
] ]
const photoStyleOptions = [
{ title: '밝고 화사한', value: '밝고 화사한' },
{ title: '모던한', value: '모던' },
{ title: '미니멀한', value: '미니멀' },
{ title: '빈티지', value: '빈티지' },
{ title: '컬러풀', value: '컬러풀' },
{ title: '우아한', value: '우아한' },
{ title: '캐주얼', value: '캐주얼' }
]
// //
const getTargetTypes = (type) => { const getTargetTypes = (type) => {
if (type === 'poster') { if (type === 'poster') {
@ -820,51 +807,39 @@ const promotionEndDateRules = [
} }
] ]
// : URL - // Computed
const getValidImageUrl = (imageUrl) => { const formValid = computed(() => {
console.log('🖼️ 이미지 URL 검증:', imageUrl, typeof imageUrl) //
if (!formData.value.title || !formData.value.targetType) {
if (!imageUrl || typeof imageUrl !== 'string') { return false
console.log('❌ 이미지 URL이 문자열이 아님')
return null
} }
// - // SNS
if (imageUrl.length < 10) { if (selectedType.value === 'sns' && !formData.value.platform) {
console.log('❌ 이미지 URL이 너무 짧음:', imageUrl.length) return false
return null
} }
// URL ( ) //
if (imageUrl.startsWith('http') || if (formData.value.targetType === 'event') {
imageUrl.startsWith('data:image/') || if (!formData.value.eventName || !formData.value.startDate || !formData.value.endDate) {
imageUrl.startsWith('blob:') || return false
imageUrl.startsWith('//') || }
imageUrl.includes('blob.core.windows.net')) { // Azure Blob Storage
console.log('✅ 유효한 이미지 URL:', imageUrl.substring(0, 50) + '...')
return imageUrl
} }
console.log('❌ 유효하지 않은 이미지 URL 형식') //
return null if (selectedType.value === 'poster') {
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) {
return false
}
//
if (!previewImages.value || previewImages.value.length === 0) {
return false
}
} }
// return true
const previewImage = (imageUrl, title) => { })
if (!imageUrl) return
//
window.open(imageUrl, '_blank')
}
// :
const handleImageError = (event) => {
console.error('❌ 이미지 로딩 실패:', event.target?.src)
// (v-img error slot )
}
// : canGenerate computed
const canGenerate = computed(() => { const canGenerate = computed(() => {
try { try {
// //
@ -894,7 +869,6 @@ const canGenerate = computed(() => {
} }
}) })
// Computed
const currentVersion = computed(() => { const currentVersion = computed(() => {
return generatedVersions.value[selectedVersion.value] || null return generatedVersions.value[selectedVersion.value] || null
}) })
@ -905,7 +879,6 @@ const selectContentType = (type) => {
console.log(`${type} 타입 선택됨`) console.log(`${type} 타입 선택됨`)
} }
// : handleFileUpload -
const handleFileUpload = (files) => { const handleFileUpload = (files) => {
console.log('📁 파일 업로드 이벤트:', files) console.log('📁 파일 업로드 이벤트:', files)
@ -979,135 +952,159 @@ const removeImage = (index) => {
} }
} }
// : generateContent - Java
const generateContent = async () => { const generateContent = async () => {
if (!canGenerate.value || remainingGenerations.value <= 0) { if (!formValid.value) {
console.log('⚠️ 생성 조건을 만족하지 않음') appStore.showSnackbar('모든 필수 항목을 입력해주세요.', 'warning')
return return
} }
// 3 if (remainingGenerations.value <= 0) {
if (generatedVersions.value.length >= 3) { appStore.showSnackbar('생성 가능 횟수를 모두 사용했습니다.', 'warning')
appStore.showSnackbar('최대 3개의 버전까지만 생성할 수 있습니다.', 'warning')
return return
} }
isGenerating.value = true
try { try {
console.log('🎯 콘텐츠 생성 시작') console.log('🚀 [UI] 콘텐츠 생성 시작')
console.log('📋 [UI] 폼 데이터:', formData.value)
console.log('📁 [UI] 이미지 데이터:', previewImages.value)
// // ID
let contentData let storeId = 1 //
if (selectedType.value === 'poster') { try {
// Java PosterContentCreateRequest // localStorage
contentData = { const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
type: selectedType.value, const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
contentType: selectedType.value,
// Java (PosterContentCreateRequest ) if (storeInfo.storeId) {
storeId: 1, storeId = storeInfo.storeId
title: formData.value.title, } else if (userInfo.storeId) {
targetAudience: convertTargetAudienceToKorean(formData.value.targetType), storeId = userInfo.storeId
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 { } else {
// Java SnsContentCreateRequest console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId)
contentData = { }
type: selectedType.value, } catch (error) {
contentType: selectedType.value, console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId)
}
// Java (SnsContentCreateRequest ) console.log('🏪 [UI] 사용할 매장 ID:', storeId)
storeId: 1,
storeName: '샘플 매장', // Base64 URL
storeType: '음식점', const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || []
platform: formData.value.platform, console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls)
//
if (selectedType.value === 'poster' && imageUrls.length === 0) {
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
}
//
const contentData = {
title: formData.value.title, title: formData.value.title,
category: getJavaCategory(formData.value.targetType), platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'),
requirement: formData.value.requirements || `${formData.value.title}에 대한 SNS 게시물을 만들어주세요`, contentType: selectedType.value,
target: convertTargetAudienceToKorean(formData.value.targetType), type: selectedType.value,
images: previewImages.value.map(img => img.url), category: getCategory(formData.value.targetType),
requirement: formData.value.requirements || `${formData.value.title}에 대한 ${selectedType.value === 'poster' ? '포스터' : 'SNS 게시물'}를 만들어주세요`,
// targetType: formData.value.targetType,
eventName: formData.value.targetType === 'event' ? formData.value.eventName : null, targetAudience: formData.value.targetType,
startDate: convertDateTimeToDateStrict(formData.value.startDate), eventName: formData.value.eventName,
endDate: convertDateTimeToDateStrict(formData.value.endDate) eventDate: formData.value.eventDate,
} startDate: formData.value.startDate,
} endDate: formData.value.endDate,
toneAndManner: formData.value.toneAndManner || '친근함',
// undefined (Java ) emotionIntensity: formData.value.emotionIntensity || '보통',
Object.keys(contentData).forEach(key => { images: imageUrls, // Base64 URL
if (contentData[key] === undefined) { storeId: storeId // ID
delete contentData[key]
}
})
console.log('🎯 [GENERATE] Java 백엔드용 데이터:', contentData)
//
if (!contentData.title) {
throw new Error('제목은 필수입니다.')
} }
//
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
if (!contentData.targetAudience) { contentData.promotionStartDate = formData.value.promotionStartDate
throw new Error('홍보 대상은 필수입니다.') contentData.promotionEndDate = formData.value.promotionEndDate
} contentData.imageStyle = formData.value.imageStyle || '모던'
if (!contentData.promotionStartDate || !contentData.promotionEndDate) { contentData.promotionType = formData.value.promotionType
throw new Error('홍보 기간은 필수입니다.') contentData.photoStyle = formData.value.photoStyle || '밝고 화사한'
}
if (!contentData.images || contentData.images.length === 0) {
throw new Error('포스터 생성을 위해서는 이미지가 필요합니다.')
}
} else {
if (!contentData.platform) {
throw new Error('플랫폼은 필수입니다.')
}
} }
// AI - store.generateContent console.log('📤 [UI] 생성 요청 데이터:', contentData)
// contentData
if (!contentData || typeof contentData !== 'object') {
throw new Error('콘텐츠 데이터 구성에 실패했습니다.')
}
if (!Array.isArray(contentData.images)) {
console.error('❌ [UI] contentData.images가 배열이 아님!')
contentData.images = []
}
// Store
console.log('🚀 [UI] contentStore.generateContent 호출')
const generated = await contentStore.generateContent(contentData) const generated = await contentStore.generateContent(contentData)
console.log('🎯 [GENERATE] AI 생성 응답:', generated) if (!generated || !generated.success) {
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
}
if (generated && generated.success) { //
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 =>
`<div style="margin-bottom: 15px; text-align: center;">
<img src="${imageUrl}" style="width: 100%; max-width: 400px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" />
</div>`
).join('')
if (isHtmlContent(finalContent)) {
finalContent = imageHtml + finalContent
} else {
finalContent = imageHtml + `<div style="padding: 15px; font-family: 'Noto Sans KR', Arial, sans-serif; line-height: 1.6;">${finalContent.replace(/\n/g, '<br>')}</div>`
}
}
}
//
const newContent = { const newContent = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
...contentData, ...contentData,
// content: finalContent,
targetType: formData.value.targetType, posterImage: posterImageUrl, // URL
platform: selectedType.value === 'sns' ? formData.value.platform : 'poster',
content: generated.content || generated.data?.content || '생성된 콘텐츠 내용',
hashtags: generated.hashtags || generated.data?.hashtags || [], hashtags: generated.hashtags || generated.data?.hashtags || [],
createdAt: new Date(), createdAt: new Date(),
status: 'draft', status: 'draft',
// posterImage uploadedImages: previewImages.value || [], //
posterImage: selectedType.value === 'poster' ? (generated.posterImage || generated.data?.posterImage || generated.content) : null images: imageUrls, // Base64 URL
platform: contentData.platform || 'POSTER'
} }
generatedVersions.value.push(newContent) generatedVersions.value.push(newContent)
selectedVersion.value = generatedVersions.value.length - 1 selectedVersion.value = generatedVersions.value.length - 1
remainingGenerations.value-- remainingGenerations.value--
console.log('✅ [GENERATE] AI 콘텐츠 생성 성공:', newContent)
appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success') appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success')
} else {
throw new Error(generated?.error || '콘텐츠 생성에 실패했습니다.')
}
} catch (error) { } catch (error) {
console.error('❌ [GENERATE] 콘텐츠 생성 실패:', error) console.error('❌ [UI] 콘텐츠 생성 실패:', error)
appStore.showSnackbar(`콘텐츠 생성 중 오류가 발생했습니다: ${error.message}`, 'error') console.error('❌ [UI] 에러 스택:', error.stack)
appStore.showSnackbar(error.message || '콘텐츠 생성 중 오류가 발생했습니다.', 'error')
} finally {
isGenerating.value = false
} }
} }
@ -1116,7 +1113,9 @@ const getCategory = (targetType) => {
'new_menu': '메뉴소개', 'new_menu': '메뉴소개',
'discount': '이벤트', 'discount': '이벤트',
'store': '인테리어', 'store': '인테리어',
'event': '이벤트' 'event': '이벤트',
'menu': '메뉴소개',
'service': '서비스'
} }
return mapping[targetType] || '기타' return mapping[targetType] || '기타'
} }
@ -1132,24 +1131,141 @@ const saveVersion = async (index) => {
try { try {
const version = generatedVersions.value[index] const version = generatedVersions.value[index]
// contentStore.saveContent console.log('💾 [UI] 저장할 버전 데이터:', version)
const saveData = {
type: version.type || version.contentType, // ID
contentType: version.contentType || version.type, let storeId = 1 //
title: version.title,
content: version.content, try {
hashtags: version.hashtags, const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
platform: version.platform, const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
category: getCategory(version.targetType),
eventName: version.eventName, if (storeInfo.storeId) {
eventDate: version.eventDate, storeId = storeInfo.storeId
status: 'PUBLISHED', } else if (userInfo.storeId) {
storeId: version.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)
//
let imageUrls = []
// URL
if (selectedType.value === 'poster') {
// 1. URL
if (version.posterImage) {
imageUrls.push(version.posterImage)
console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage)
}
// 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)
}
// 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 {
// 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)
if (result.success) {
version.status = 'published' version.status = 'published'
version.publishedAt = new Date() version.publishedAt = new Date()
@ -1160,9 +1276,6 @@ const saveVersion = async (index) => {
router.push('/content') router.push('/content')
} }
}, 1000) }, 1000)
} else {
throw new Error(result.error || '저장에 실패했습니다.')
}
} catch (error) { } catch (error) {
console.error('❌ 콘텐츠 저장 실패:', error) console.error('❌ 콘텐츠 저장 실패:', error)
appStore.showSnackbar(error.message || '콘텐츠 저장 중 오류가 발생했습니다.', 'error') appStore.showSnackbar(error.message || '콘텐츠 저장 중 오류가 발생했습니다.', 'error')
@ -1216,7 +1329,8 @@ const getPlatformIcon = (platform) => {
'INSTAGRAM': 'mdi-instagram', 'INSTAGRAM': 'mdi-instagram',
'NAVER_BLOG': 'mdi-web', 'NAVER_BLOG': 'mdi-web',
'FACEBOOK': 'mdi-facebook', 'FACEBOOK': 'mdi-facebook',
'KAKAO_STORY': 'mdi-chat' 'KAKAO_STORY': 'mdi-chat',
'POSTER': 'mdi-image'
} }
return icons[platform] || 'mdi-web' return icons[platform] || 'mdi-web'
} }
@ -1230,7 +1344,8 @@ const getPlatformColor = (platform) => {
'INSTAGRAM': 'pink', 'INSTAGRAM': 'pink',
'NAVER_BLOG': 'green', 'NAVER_BLOG': 'green',
'FACEBOOK': 'blue', 'FACEBOOK': 'blue',
'KAKAO_STORY': 'amber' 'KAKAO_STORY': 'amber',
'POSTER': 'orange'
} }
return colors[platform] || 'grey' return colors[platform] || 'grey'
} }
@ -1244,90 +1359,12 @@ const getPlatformLabel = (platform) => {
'INSTAGRAM': '인스타그램', 'INSTAGRAM': '인스타그램',
'NAVER_BLOG': '네이버 블로그', 'NAVER_BLOG': '네이버 블로그',
'FACEBOOK': '페이스북', 'FACEBOOK': '페이스북',
'KAKAO_STORY': '카카오스토리' 'KAKAO_STORY': '카카오스토리',
'POSTER': '포스터'
} }
return labels[platform] || platform 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 getStatusColor = (status) => {
const colors = { const colors = {
'draft': 'grey', 'draft': 'grey',
@ -1396,6 +1433,15 @@ const truncateHtmlContent = (html, maxLength) => {
return `<div style="padding: 10px; font-family: 'Noto Sans KR', Arial, sans-serif;">${truncateText(textContent, maxLength)}</div>` return `<div style="padding: 10px; font-family: 'Noto Sans KR', Arial, sans-serif;">${truncateText(textContent, maxLength)}</div>`
} }
const previewImage = (imageUrl, title) => {
if (!imageUrl) return
window.open(imageUrl, '_blank')
}
const handleImageError = (event) => {
console.error('❌ 이미지 로딩 실패:', event.target?.src)
}
// //
onMounted(() => { onMounted(() => {
console.log('📱 콘텐츠 생성 페이지 로드됨') console.log('📱 콘텐츠 생성 페이지 로드됨')
@ -1482,3 +1528,4 @@ onMounted(() => {
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@ -152,8 +152,22 @@
{{ getStatusText(content.status) }} {{ getStatusText(content.status) }}
</v-chip> </v-chip>
</div> </div>
<!-- 포스터인 경우와 SNS인 경우 구분하여 미리보기 표시 -->
<div class="text-caption text-truncate grey--text" style="max-width: 400px;"> <div class="text-caption text-truncate grey--text" style="max-width: 400px;">
{{ content.content ? content.content.substring(0, 100) + '...' : '' }} <div v-if="content.platform === 'POSTER' || content.platform === 'poster'">
<div v-if="content.content && isImageUrl(content.content)" class="d-flex align-center">
<v-icon size="16" color="primary" class="mr-1">mdi-image</v-icon>
<span>포스터 이미지 생성됨</span>
</div>
<div v-else>
{{ content.content ? content.content.substring(0, 100) + '...' : '포스터 콘텐츠' }}
</div>
</div>
<!-- SNS인 경우 HTML 태그 제거하고 텍스트만 표시 -->
<div v-else>
{{ content.content ? getPlainTextPreview(content.content) : '' }}
</div>
</div> </div>
</div> </div>
</td> </td>
@ -272,9 +286,61 @@
</v-chip> </v-chip>
</div> </div>
<!-- 포스터인 경우 이미지로 표시, SNS인 경우 HTML 렌더링 -->
<div class="mb-4"> <div class="mb-4">
<div class="text-subtitle-2 text-grey-600 mb-1">내용</div> <div class="text-subtitle-2 text-grey-600 mb-1">내용</div>
<div class="text-body-1 content-preview">{{ selectedContent.content }}</div>
<!-- 포스터인 경우 이미지로 표시 -->
<div v-if="selectedContent.platform === 'POSTER' || selectedContent.platform === 'poster'">
<v-img
v-if="selectedContent.content && isImageUrl(selectedContent.content)"
:src="selectedContent.content"
:alt="selectedContent.title"
cover
class="rounded-lg elevation-2"
style="max-width: 400px; aspect-ratio: 3/4; cursor: pointer;"
@click="previewImage(selectedContent.content, selectedContent.title)"
@error="handleImageError"
>
<template v-slot:placeholder>
<div class="d-flex align-center justify-center fill-height bg-grey-lighten-4">
<v-progress-circular indeterminate color="primary" size="32" />
<span class="ml-2 text-grey">이미지 로딩 ...</span>
</div>
</template>
<template v-slot:error>
<div class="d-flex flex-column align-center justify-center fill-height bg-grey-lighten-3">
<v-icon size="32" color="grey" class="mb-2">mdi-image-broken</v-icon>
<span class="text-caption text-grey">이미지를 불러올 없습니다</span>
<span class="text-caption text-grey mt-1" style="word-break: break-all; max-width: 300px;">
{{ selectedContent.content?.substring(0, 50) }}...
</span>
</div>
</template>
</v-img>
<div v-else class="d-flex flex-column align-center justify-center bg-grey-lighten-4 rounded-lg pa-8">
<v-icon size="48" color="grey" class="mb-2">mdi-image-off</v-icon>
<span class="text-body-2 text-grey">포스터 이미지가 없습니다</span>
<span class="text-caption text-grey mt-1" v-if="selectedContent.content">
URL: {{ selectedContent.content }}
</span>
</div>
</div>
<!-- SNS인 경우 HTML 렌더링으로 표시 -->
<div v-else>
<!-- HTML 콘텐츠가 있는 경우 렌더링하여 표시 -->
<div v-if="isHtmlContent(selectedContent.content)"
class="content-preview html-content"
v-html="selectedContent.content">
</div>
<!-- 일반 텍스트인 경우 그대로 표시 -->
<div v-else class="text-body-1 content-preview">
{{ selectedContent.content }}
</div>
</div>
</div> </div>
<div class="mb-4" v-if="selectedContent.hashtags && selectedContent.hashtags.length > 0"> <div class="mb-4" v-if="selectedContent.hashtags && selectedContent.hashtags.length > 0">
@ -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 getStatusColor = (status) => {
const statusColors = { const statusColors = {
'DRAFT': 'orange', 'DRAFT': 'orange',
@ -783,4 +920,61 @@ onMounted(() => {
.cursor-pointer { .cursor-pointer {
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;
}
</style> </style>

View File

@ -252,12 +252,7 @@
<div v-else-if="aiRecommendation" class="ai-recommendation-content"> <div v-else-if="aiRecommendation" class="ai-recommendation-content">
<!-- 추천 제목 --> <!-- 추천 제목 -->
<div class="recommendation-header mb-4"> <div class="recommendation-header mb-4">
<div class="d-flex align-center mb-2">
<span class="recommendation-emoji mr-2">{{ aiRecommendation.emoji }}</span>
<h4 class="text-h6 font-weight-bold text-primary">
{{ aiRecommendation.title }}
</h4>
</div>
</div> </div>
<!-- 스크롤 가능한 콘텐츠 영역 --> <!-- 스크롤 가능한 콘텐츠 영역 -->

View File

@ -167,7 +167,7 @@ const generatePoster = async (formData) => {
} }
/** /**
* 포스터 저장 * 포스터 저장 - 수정된 버전
*/ */
const savePoster = async () => { const savePoster = async () => {
if (!generatedPoster.value) { if (!generatedPoster.value) {
@ -181,12 +181,22 @@ const savePoster = async () => {
loadingMessage.value = '포스터 저장 중...' loadingMessage.value = '포스터 저장 중...'
loadingSubMessage.value = '잠시만 기다려주세요' loadingSubMessage.value = '잠시만 기다려주세요'
// contentId
const result = await posterStore.savePoster({ const result = await posterStore.savePoster({
contentId: generatedPoster.value.contentId, // contentId: ID ( @NotNull)
contentId: generatedPoster.value.contentId || Date.now(), // ID
storeId: authStore.currentStore?.id || 1, storeId: authStore.currentStore?.id || 1,
title: posterForm.value.title, 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', status: 'PUBLISHED',
category: posterForm.value.category, category: posterForm.value.category,
requirement: posterForm.value.requirement, requirement: posterForm.value.requirement,