Merge branch 'main' of https://github.com/won-ktds/smarketing-frontend
This commit is contained in:
commit
8ff40e3beb
@ -28,43 +28,58 @@
|
||||
<div v-else class="poster-result">
|
||||
<!-- 포스터 이미지 -->
|
||||
<div class="poster-image-container mb-4">
|
||||
<!-- ✅ 이미지 URL 유효성 검사 후 렌더링 -->
|
||||
<v-img
|
||||
:src="posterData.posterImage || '/images/placeholder-poster.jpg'"
|
||||
v-if="getPosterImageUrl()"
|
||||
:src="getPosterImageUrl()"
|
||||
:alt="posterData.title"
|
||||
cover
|
||||
class="rounded-lg elevation-4"
|
||||
style="aspect-ratio: 3/4; max-height: 400px;"
|
||||
class="rounded-lg elevation-4 poster-image"
|
||||
style="aspect-ratio: 3/4; max-height: 400px; width: 100%;"
|
||||
>
|
||||
<template v-slot:placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular indeterminate />
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<span class="ml-2">이미지 로딩 중...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:error>
|
||||
<div class="d-flex align-center justify-center fill-height bg-grey-lighten-3">
|
||||
<v-icon size="48" color="grey">mdi-image-broken</v-icon>
|
||||
<div class="d-flex flex-column align-center justify-center fill-height bg-grey-lighten-3">
|
||||
<v-icon size="48" color="grey" class="mb-2">mdi-image-broken</v-icon>
|
||||
<span class="text-body-2 text-grey">이미지를 불러올 수 없습니다</span>
|
||||
<span class="text-caption text-grey mt-1">{{ getPosterImageUrl() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
|
||||
<!-- ✅ 이미지가 없거나 유효하지 않은 경우 -->
|
||||
<div v-else class="d-flex flex-column align-center justify-center fill-height bg-grey-lighten-4 rounded-lg" style="aspect-ratio: 3/4; max-height: 400px;">
|
||||
<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="posterData.posterImage">
|
||||
URL: {{ posterData.posterImage }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 이미지 액션 버튼 -->
|
||||
<div class="image-actions mt-3">
|
||||
<div class="image-actions mt-4 d-flex gap-2 justify-center">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-download"
|
||||
@click="downloadPoster"
|
||||
class="mr-2"
|
||||
:disabled="!getPosterImageUrl()"
|
||||
>
|
||||
다운로드
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-share-variant"
|
||||
@click="sharePoster"
|
||||
:disabled="!getPosterImageUrl()"
|
||||
>
|
||||
공유
|
||||
</v-btn>
|
||||
@ -72,97 +87,95 @@
|
||||
</div>
|
||||
|
||||
<!-- 포스터 정보 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card class="mb-4" variant="outlined">
|
||||
<v-card-title class="text-h6">
|
||||
{{ posterData.title }}
|
||||
{{ posterData.title || '제목 없음' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div v-if="posterData.content" class="mb-3">
|
||||
<div class="text-subtitle-2 mb-1">포스터 내용:</div>
|
||||
<div class="text-body-2">{{ posterData.content }}</div>
|
||||
<div class="poster-details">
|
||||
<div v-if="posterData.targetAudience" class="detail-item mb-2">
|
||||
<v-icon class="mr-2" size="small">mdi-target</v-icon>
|
||||
<span class="text-body-2">
|
||||
<strong>홍보 대상:</strong> {{ posterData.targetAudience }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center mb-2">
|
||||
<div v-if="posterData.imageStyle" class="detail-item mb-2">
|
||||
<v-icon class="mr-2" size="small">mdi-palette</v-icon>
|
||||
<span class="text-body-2">
|
||||
<strong>이미지 스타일:</strong> {{ posterData.imageStyle }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="posterData.category" class="detail-item mb-2">
|
||||
<v-icon class="mr-2" size="small">mdi-tag</v-icon>
|
||||
<span class="text-body-2">
|
||||
<strong>카테고리:</strong> {{ posterData.category }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="posterData.promotionStartDate" class="detail-item mb-2">
|
||||
<v-icon class="mr-2" size="small">mdi-calendar-start</v-icon>
|
||||
<span class="text-body-2">
|
||||
<strong>홍보 기간:</strong>
|
||||
{{ formatDate(posterData.promotionStartDate) }}
|
||||
<span v-if="posterData.promotionEndDate">
|
||||
~ {{ formatDate(posterData.promotionEndDate) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="posterData.status" class="detail-item">
|
||||
<v-chip
|
||||
:color="getStatusColor(posterData.status)"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getStatusText(posterData.status) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ posterData.contentType }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="posterData.imageStyle" class="text-caption text-grey-600">
|
||||
스타일: {{ posterData.imageStyle }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 포스터 사이즈 옵션 -->
|
||||
<!-- 다양한 사이즈 포스터 (있는 경우) -->
|
||||
<v-card v-if="posterData.posterSizes && Object.keys(posterData.posterSizes).length > 0" variant="outlined">
|
||||
<v-card-title class="text-subtitle-1">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon class="mr-2">mdi-resize</v-icon>
|
||||
다양한 사이즈
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip
|
||||
<v-btn
|
||||
v-for="(url, size) in posterData.posterSizes"
|
||||
:key="size"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@click="viewPosterSize(size, url)"
|
||||
class="cursor-pointer"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ size }}
|
||||
</v-chip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 원본 이미지들 -->
|
||||
<div v-if="posterData.originalImages && posterData.originalImages.length > 0" class="mt-4">
|
||||
<div class="text-subtitle-2 mb-2">사용된 원본 이미지:</div>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="(image, index) in posterData.originalImages"
|
||||
:key="index"
|
||||
cols="6"
|
||||
sm="4"
|
||||
>
|
||||
<v-img
|
||||
:src="image"
|
||||
:alt="`원본 이미지 ${index + 1}`"
|
||||
cover
|
||||
height="80"
|
||||
class="rounded"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 포스터 사이즈 보기 다이얼로그 -->
|
||||
<v-dialog v-model="showSizeDialog" max-width="600">
|
||||
<!-- 사이즈별 포스터 보기 다이얼로그 -->
|
||||
<v-dialog v-model="showSizeDialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
포스터 사이즈: {{ selectedSize }}
|
||||
{{ selectedSize }} 포스터
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="text-center">
|
||||
<v-card-text>
|
||||
<v-img
|
||||
:src="selectedSizeUrl"
|
||||
:alt="`포스터 ${selectedSize}`"
|
||||
contain
|
||||
max-height="400"
|
||||
cover
|
||||
class="rounded-lg"
|
||||
style="max-height: 500px;"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
@ -185,7 +198,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
/**
|
||||
* 포스터 미리보기 컴포넌트
|
||||
@ -211,6 +224,38 @@ const showSizeDialog = ref(false)
|
||||
const selectedSize = ref('')
|
||||
const selectedSizeUrl = ref('')
|
||||
|
||||
/**
|
||||
* ✅ 포스터 이미지 URL 검증 및 반환
|
||||
*/
|
||||
const getPosterImageUrl = () => {
|
||||
if (!props.posterData) return null
|
||||
|
||||
const posterImage = props.posterData.posterImage
|
||||
|
||||
// 디버깅을 위한 로그
|
||||
console.log('🖼️ [PosterPreview] 이미지 URL 검증:', {
|
||||
posterImage,
|
||||
type: typeof posterImage,
|
||||
length: posterImage?.length,
|
||||
isString: typeof posterImage === 'string',
|
||||
isValidUrl: posterImage && typeof posterImage === 'string' && posterImage.length > 10
|
||||
})
|
||||
|
||||
// URL 유효성 검사
|
||||
if (posterImage && typeof posterImage === 'string' && posterImage.length > 10) {
|
||||
// HTTP(S) URL 또는 Data URL 확인
|
||||
if (posterImage.startsWith('http') ||
|
||||
posterImage.startsWith('data:image/') ||
|
||||
posterImage.startsWith('blob:') ||
|
||||
posterImage.startsWith('//')) {
|
||||
return posterImage
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('⚠️ [PosterPreview] 유효하지 않은 이미지 URL:', posterImage)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 색상 반환
|
||||
*/
|
||||
@ -236,42 +281,83 @@ const getStatusText = (status) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 포스터 다운로드
|
||||
* 날짜 포맷팅
|
||||
*/
|
||||
const downloadPoster = () => {
|
||||
if (!props.posterData?.posterImage) return
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = props.posterData.posterImage
|
||||
link.download = `${props.posterData.title || '포스터'}.jpg`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
} catch (error) {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 포스터 공유
|
||||
* ✅ 포스터 다운로드 - URL 검증 추가
|
||||
*/
|
||||
const downloadPoster = () => {
|
||||
const imageUrl = getPosterImageUrl()
|
||||
if (!imageUrl) {
|
||||
console.error('❌ [PosterPreview] 다운로드할 이미지 URL이 없습니다')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('📥 [PosterPreview] 포스터 다운로드 시도:', imageUrl)
|
||||
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = imageUrl
|
||||
link.download = `${props.posterData.title || '포스터'}.jpg`
|
||||
link.target = '_blank' // 새 탭에서 열기
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
console.log('✅ [PosterPreview] 다운로드 링크 클릭 완료')
|
||||
} catch (error) {
|
||||
console.error('❌ [PosterPreview] 다운로드 실패:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ 포스터 공유 - URL 검증 추가
|
||||
*/
|
||||
const sharePoster = async () => {
|
||||
if (!props.posterData?.posterImage) return
|
||||
const imageUrl = getPosterImageUrl()
|
||||
if (!imageUrl) {
|
||||
console.error('❌ [PosterPreview] 공유할 이미지 URL이 없습니다')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔗 [PosterPreview] 포스터 공유 시도:', imageUrl)
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: props.posterData.title,
|
||||
title: props.posterData.title || '생성된 포스터',
|
||||
text: '생성된 홍보 포스터를 확인해보세요!',
|
||||
url: props.posterData.posterImage
|
||||
url: imageUrl
|
||||
})
|
||||
console.log('✅ [PosterPreview] 공유 완료')
|
||||
} catch (error) {
|
||||
console.log('공유 취소됨')
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('❌ [PosterPreview] 공유 실패:', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 클립보드에 URL 복사
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.posterData.posterImage)
|
||||
// 성공 알림 표시 (부모 컴포넌트에서 처리)
|
||||
await navigator.clipboard.writeText(imageUrl)
|
||||
console.log('✅ [PosterPreview] 클립보드 복사 완료')
|
||||
// 성공 알림은 부모 컴포넌트에서 처리
|
||||
} catch (error) {
|
||||
console.error('클립보드 복사 실패:', error)
|
||||
console.error('❌ [PosterPreview] 클립보드 복사 실패:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -289,9 +375,12 @@ const viewPosterSize = (size, url) => {
|
||||
* 선택된 사이즈 포스터 다운로드
|
||||
*/
|
||||
const downloadSelectedSize = () => {
|
||||
if (!selectedSizeUrl.value) return
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = selectedSizeUrl.value
|
||||
link.download = `${props.posterData.title || '포스터'}_${selectedSize.value}.jpg`
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
@ -307,12 +396,50 @@ const downloadSelectedSize = () => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 호버 효과 */
|
||||
.v-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 이미지 로딩 애니메이션 */
|
||||
.v-img {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* 상세 정보 스타일링 */
|
||||
.poster-details .detail-item {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.poster-details .detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
//* src/store/content.js - 두 파일 완전 통합 버전 (Part 1)
|
||||
//* src/store/content.js - 콘텐츠 관리 기능이 통합된 최종 버전
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import contentService from '@/services/content'
|
||||
@ -203,32 +203,91 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 목록 조회 (기존 호환성 유지)
|
||||
*/
|
||||
const fetchContentList = async (requestFilters = {}) => {
|
||||
console.log('📋 [STORE] fetchContentList 호출:', requestFilters)
|
||||
return await loadContents(requestFilters)
|
||||
}
|
||||
// ===== 기존 API 호출 함수들을 통합된 방식으로 수정 =====
|
||||
|
||||
// ===== AI 콘텐츠 생성 =====
|
||||
/**
|
||||
* SNS 콘텐츠 생성 (API 설계서 기준) - 이미지 디버깅 강화
|
||||
* generateContent를 실제 API 호출로 수정 - 단일 파라미터로 변경하고 contentService.generateContent 사용
|
||||
*/
|
||||
const generateSnsContent = async (contentData) => {
|
||||
const generateContent = async (contentData) => {
|
||||
generating.value = true
|
||||
|
||||
try {
|
||||
console.log('🎯 [STORE] SNS 콘텐츠 생성 요청:', contentData)
|
||||
console.log('📁 [STORE] SNS 이미지 확인:', {
|
||||
hasImages: !!contentData.images,
|
||||
imageCount: contentData.images?.length || 0,
|
||||
imageTypes: contentData.images?.map(img => typeof img) || [],
|
||||
imageSizes: contentData.images?.map(img => img?.length || 'unknown') || []
|
||||
})
|
||||
console.log('🎯 [STORE] generateContent 호출됨:', contentData)
|
||||
|
||||
// contentService의 통합 generateContent 함수 사용
|
||||
const result = await contentService.generateContent(contentData)
|
||||
|
||||
console.log('🎯 [STORE] API 응답:', result)
|
||||
|
||||
if (result && result.success) {
|
||||
generatedContent.value = result.data
|
||||
return {
|
||||
success: true,
|
||||
data: result.data,
|
||||
content: result.data?.content || result.data?.text,
|
||||
hashtags: result.data?.hashtags || []
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || 'API 응답이 올바르지 않습니다.'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] generateContent 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* saveContent를 실제 API 호출로 수정 - 단일 파라미터로 변경
|
||||
*/
|
||||
const saveContent = async (contentData) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
console.log('💾 [STORE] saveContent 호출됨:', contentData)
|
||||
|
||||
// contentService의 통합 saveContent 함수 사용
|
||||
const result = await contentService.saveContent(contentData)
|
||||
|
||||
if (result && result.success) {
|
||||
// 콘텐츠 목록 새로고침
|
||||
await fetchContentList()
|
||||
return { success: true, message: '콘텐츠가 저장되었습니다.' }
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || '저장에 실패했습니다.'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] saveContent 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 포스터 저장 - 수정된 버전
|
||||
*/
|
||||
const savePoster = async (saveData) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
console.log('💾 [STORE] 포스터 저장 요청:', saveData)
|
||||
|
||||
// 매장 ID 조회 (필요한 경우)
|
||||
let storeId = contentData.storeId
|
||||
let storeId = saveData.storeId
|
||||
if (!storeId) {
|
||||
try {
|
||||
storeId = await getStoreId()
|
||||
@ -238,282 +297,64 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// SnsContentCreateRequest 구조에 맞게 데이터 변환
|
||||
// ✅ PosterContentSaveRequest 구조에 맞게 데이터 변환 (contentId 처리 개선)
|
||||
const requestData = {
|
||||
// ✅ contentId 처리: 값이 있으면 사용, 없으면 임시 ID 생성
|
||||
contentId: saveData.contentId || Date.now(),
|
||||
storeId: storeId,
|
||||
storeName: contentData.storeName || '',
|
||||
storeType: contentData.storeType || '',
|
||||
platform: contentData.platform || 'INSTAGRAM',
|
||||
title: contentData.title || 'SNS 게시물',
|
||||
category: contentData.category || '메뉴소개',
|
||||
requirement: contentData.requirement || contentData.requirements || 'SNS 게시물을 생성해주세요',
|
||||
target: contentData.target || contentData.targetAudience || '',
|
||||
contentType: contentData.contentType || 'SNS 게시물',
|
||||
eventName: contentData.eventName || '',
|
||||
startDate: contentData.startDate,
|
||||
endDate: contentData.endDate,
|
||||
images: contentData.images || [],
|
||||
photoStyle: contentData.photoStyle || '밝고 화사한',
|
||||
targetAge: contentData.targetAge || '20대',
|
||||
toneAndManner: contentData.toneAndManner || '친근함',
|
||||
emotionalIntensity: contentData.emotionalIntensity || contentData.emotionIntensity || '보통',
|
||||
promotionalType: contentData.promotionalType || contentData.promotionType || '',
|
||||
eventDate: contentData.eventDate,
|
||||
hashtagStyle: contentData.hashtagStyle || '',
|
||||
hashtagCount: contentData.hashtagCount || 10,
|
||||
contentLength: contentData.contentLength || '보통',
|
||||
includeHashtags: contentData.includeHashtags !== false,
|
||||
includeEmojis: contentData.includeEmojis !== false,
|
||||
includeEmoji: contentData.includeEmoji !== false,
|
||||
includeCallToAction: contentData.includeCallToAction !== false,
|
||||
includeLocation: contentData.includeLocation || false,
|
||||
forInstagramStory: contentData.forInstagramStory || false,
|
||||
forNaverBlogPost: contentData.forNaverBlogPost || false,
|
||||
alternativeTitleCount: contentData.alternativeTitleCount || 3,
|
||||
alternativeHashtagSetCount: contentData.alternativeHashtagSetCount || 2,
|
||||
preferredAiModel: contentData.preferredAiModel || ''
|
||||
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 || '밝고 화사한'
|
||||
}
|
||||
|
||||
console.log('📤 [STORE] SNS 변환된 요청 데이터:', {
|
||||
// ✅ 필수 필드 검증
|
||||
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.generateSnsContent(requestData)
|
||||
const result = await contentService.savePoster(requestData)
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ [STORE] SNS 콘텐츠 생성 성공:', result.data)
|
||||
generatedContent.value = result.data
|
||||
console.log('✅ [STORE] 포스터 저장 성공')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: result.data.content || '콘텐츠가 생성되었습니다.',
|
||||
hashtags: result.data.hashtags || [],
|
||||
data: result.data
|
||||
}
|
||||
// 목록 새로고침
|
||||
await loadContents()
|
||||
|
||||
return { success: true, message: '포스터가 저장되었습니다.' }
|
||||
} else {
|
||||
console.error('❌ [STORE] SNS 콘텐츠 생성 실패:', result.message)
|
||||
return {
|
||||
success: false,
|
||||
message: result.message || 'SNS 콘텐츠 생성에 실패했습니다.',
|
||||
error: result.error
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] SNS 콘텐츠 생성 예외:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '네트워크 오류가 발생했습니다.',
|
||||
error: error
|
||||
}
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
//* src/store/content.js - 두 파일 완전 통합 버전 (Part 2)
|
||||
/**
|
||||
* 포스터 생성 (API 설계서 기준) - 이미지 디버깅 대폭 강화
|
||||
*/
|
||||
const generatePoster = async (posterData) => {
|
||||
generating.value = true
|
||||
|
||||
try {
|
||||
console.log('🎯 [STORE] 포스터 생성 요청 받음:', posterData)
|
||||
console.log('📁 [STORE] 포스터 이미지 상세 분석:', {
|
||||
hasImages: !!posterData.images,
|
||||
imageCount: posterData.images?.length || 0,
|
||||
imageArray: Array.isArray(posterData.images),
|
||||
firstImageInfo: posterData.images?.[0] ? {
|
||||
type: typeof posterData.images[0],
|
||||
length: posterData.images[0]?.length || 'unknown',
|
||||
isBase64: posterData.images[0]?.startsWith?.('data:image/') || false,
|
||||
preview: posterData.images[0]?.substring(0, 50) + '...'
|
||||
} : null
|
||||
})
|
||||
|
||||
// 매장 ID 조회 (필요한 경우)
|
||||
let storeId = posterData.storeId
|
||||
if (storeId === undefined || storeId === null) {
|
||||
try {
|
||||
storeId = await getStoreId()
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error)
|
||||
storeId = 1
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 실제 전달받은 데이터만 사용 (기본값 완전 제거)
|
||||
const requestData = {}
|
||||
|
||||
// 조건부로 필드 추가 (값이 있을 때만)
|
||||
if (storeId !== undefined) {
|
||||
requestData.storeId = storeId
|
||||
console.log('📝 [STORE] storeId 추가:', requestData.storeId)
|
||||
}
|
||||
|
||||
if (posterData.title) {
|
||||
requestData.title = posterData.title
|
||||
console.log('📝 [STORE] title 추가:', requestData.title)
|
||||
}
|
||||
|
||||
if (posterData.targetAudience) {
|
||||
requestData.targetAudience = posterData.targetAudience
|
||||
console.log('📝 [STORE] targetAudience 추가:', requestData.targetAudience)
|
||||
} else if (posterData.targetType) {
|
||||
requestData.targetAudience = posterData.targetType
|
||||
console.log('📝 [STORE] targetAudience 추가 (from targetType):', requestData.targetAudience)
|
||||
}
|
||||
|
||||
if (posterData.promotionStartDate) {
|
||||
requestData.promotionStartDate = posterData.promotionStartDate
|
||||
console.log('📝 [STORE] promotionStartDate 추가:', requestData.promotionStartDate)
|
||||
}
|
||||
|
||||
if (posterData.promotionEndDate) {
|
||||
requestData.promotionEndDate = posterData.promotionEndDate
|
||||
console.log('📝 [STORE] promotionEndDate 추가:', requestData.promotionEndDate)
|
||||
}
|
||||
|
||||
// 선택적 필드들
|
||||
if (posterData.eventName) {
|
||||
requestData.eventName = posterData.eventName
|
||||
console.log('📝 [STORE] eventName 추가:', requestData.eventName)
|
||||
}
|
||||
|
||||
if (posterData.imageStyle) requestData.imageStyle = posterData.imageStyle
|
||||
if (posterData.promotionType) requestData.promotionType = posterData.promotionType
|
||||
if (posterData.emotionIntensity) requestData.emotionIntensity = posterData.emotionIntensity
|
||||
if (posterData.category) requestData.category = posterData.category
|
||||
if (posterData.requirement || posterData.requirements) {
|
||||
requestData.requirement = posterData.requirement || posterData.requirements
|
||||
}
|
||||
if (posterData.toneAndManner) requestData.toneAndManner = posterData.toneAndManner
|
||||
if (posterData.startDate) requestData.startDate = posterData.startDate
|
||||
if (posterData.endDate) requestData.endDate = posterData.endDate
|
||||
if (posterData.photoStyle) requestData.photoStyle = posterData.photoStyle
|
||||
if (posterData.targetAge) {
|
||||
requestData.targetAge = posterData.targetAge
|
||||
console.log('📝 [STORE] targetAge 추가:', requestData.targetAge)
|
||||
}
|
||||
|
||||
// ✅ 이미지 처리 - 가장 중요한 부분
|
||||
console.log('📁 [STORE] 이미지 처리 시작...')
|
||||
if (posterData.images && Array.isArray(posterData.images) && posterData.images.length > 0) {
|
||||
console.log('📁 [STORE] 원본 이미지 배열:', posterData.images.length, '개')
|
||||
|
||||
// 유효한 이미지만 필터링
|
||||
const validImages = posterData.images.filter(img => {
|
||||
const isValid = img && typeof img === 'string' && img.length > 0
|
||||
console.log('📁 [STORE] 이미지 유효성 검사:', {
|
||||
isValid,
|
||||
type: typeof img,
|
||||
length: img?.length,
|
||||
isBase64: img?.startsWith?.('data:image/')
|
||||
})
|
||||
return isValid
|
||||
})
|
||||
|
||||
requestData.images = validImages
|
||||
console.log('📁 [STORE] 필터링된 이미지:', validImages.length, '개')
|
||||
console.log('📁 [STORE] 첫 번째 이미지 샘플:', validImages[0]?.substring(0, 100) + '...')
|
||||
} else {
|
||||
requestData.images = []
|
||||
console.warn('📁 [STORE] ⚠️ 이미지가 없거나 유효하지 않음!')
|
||||
console.warn('📁 [STORE] posterData.images:', posterData.images)
|
||||
}
|
||||
|
||||
console.log('🔍 [STORE] 최종 요청 데이터 확인:')
|
||||
console.log(' - 제목:', requestData.title)
|
||||
console.log(' - 홍보 대상:', requestData.targetAudience)
|
||||
console.log(' - 타겟 연령층:', requestData.targetAge)
|
||||
console.log(' - 홍보 시작일:', requestData.promotionStartDate)
|
||||
console.log(' - 홍보 종료일:', requestData.promotionEndDate)
|
||||
console.log(' - 이미지 개수:', requestData.images.length)
|
||||
|
||||
if (requestData.images.length === 0) {
|
||||
console.error('❌ [STORE] 포스터에 이미지가 없습니다!')
|
||||
return {
|
||||
success: false,
|
||||
message: '포스터 생성을 위해서는 최소 1개의 이미지가 필요합니다.'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await contentService.generatePoster(requestData)
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ [STORE] 포스터 생성 성공:', result.data)
|
||||
generatedContent.value = result.data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: result.data.posterImage || result.data.content || '포스터가 생성되었습니다.',
|
||||
posterImage: result.data.posterImage,
|
||||
title: result.data.title,
|
||||
data: result.data
|
||||
}
|
||||
} else {
|
||||
console.error('❌ [STORE] 포스터 생성 실패:', result.message)
|
||||
return {
|
||||
success: false,
|
||||
message: result.message || '포스터 생성에 실패했습니다.',
|
||||
error: result.error
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] 포스터 생성 예외:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '네트워크 오류가 발생했습니다.',
|
||||
error: error
|
||||
}
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 콘텐츠 생성 통합 메서드 (기존 호환성 유지)
|
||||
*/
|
||||
const generateContent = async (type, formData) => {
|
||||
console.log('🎯 [STORE] 콘텐츠 생성 요청 (통합):', { type, formData })
|
||||
console.log('📁 [STORE] 통합 메서드 이미지 확인:', {
|
||||
hasImages: !!formData.images,
|
||||
imageCount: formData.images?.length || 0
|
||||
})
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
let result
|
||||
|
||||
// 타입에 따라 적절한 메서드 호출
|
||||
if (type === 'poster' || formData.contentType === 'poster' || formData.type === 'poster') {
|
||||
console.log('🎯 [STORE] 포스터 생성으로 라우팅')
|
||||
result = await generatePoster(formData)
|
||||
} else if (type === 'sns' || type === 'snsContent') {
|
||||
console.log('🎯 [STORE] SNS 생성으로 라우팅')
|
||||
result = await generateSnsContent(formData)
|
||||
} else {
|
||||
console.log('🎯 [STORE] 기본 SNS 생성으로 라우팅')
|
||||
result = await generateSnsContent(formData)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data || result }
|
||||
} else {
|
||||
return { success: false, error: result.message || result.error }
|
||||
console.error('❌ [STORE] 포스터 저장 실패:', result.message)
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] 통합 콘텐츠 생성 실패:', error)
|
||||
console.error('❌ [STORE] 포스터 저장 예외:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 콘텐츠 저장 =====
|
||||
/**
|
||||
* SNS 콘텐츠 저장
|
||||
*/
|
||||
@ -536,25 +377,23 @@ export const useContentStore = defineStore('content', () => {
|
||||
|
||||
// SnsContentSaveRequest 구조에 맞게 데이터 변환
|
||||
const requestData = {
|
||||
contentId: saveData.contentId,
|
||||
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 || '',
|
||||
finalTitle: saveData.finalTitle || saveData.title,
|
||||
finalContent: saveData.finalContent || saveData.content,
|
||||
status: saveData.status || 'DRAFT',
|
||||
category: saveData.category || '메뉴소개',
|
||||
requirement: saveData.requirement || '',
|
||||
toneAndManner: saveData.toneAndManner || '친근함',
|
||||
emotionIntensity: saveData.emotionIntensity || saveData.emotionalIntensity || '보통',
|
||||
emotionIntensity: saveData.emotionIntensity || '보통',
|
||||
eventName: saveData.eventName || '',
|
||||
startDate: saveData.startDate,
|
||||
endDate: saveData.endDate,
|
||||
promotionalType: saveData.promotionalType,
|
||||
eventDate: saveData.eventDate
|
||||
endDate: saveData.endDate
|
||||
}
|
||||
|
||||
const result = await contentService.saveSnsContent(requestData)
|
||||
@ -579,127 +418,39 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 포스터 저장
|
||||
* fetchContentList를 실제 API 호출로 수정 (기존 호환성 유지)
|
||||
*/
|
||||
const savePoster = async (saveData) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
console.log('💾 [STORE] 포스터 저장 요청:', saveData)
|
||||
|
||||
// 매장 ID 조회 (필요한 경우)
|
||||
let storeId = saveData.storeId
|
||||
if (!storeId) {
|
||||
try {
|
||||
storeId = await getStoreId()
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error)
|
||||
storeId = 1
|
||||
}
|
||||
}
|
||||
|
||||
// PosterContentSaveRequest 구조에 맞게 데이터 변환
|
||||
const requestData = {
|
||||
contentId: saveData.contentId,
|
||||
storeId: storeId,
|
||||
title: saveData.title || '',
|
||||
content: saveData.content || '',
|
||||
images: saveData.images || [],
|
||||
status: saveData.status || 'DRAFT',
|
||||
category: saveData.category || '이벤트',
|
||||
requirement: saveData.requirement || '',
|
||||
toneAndManner: saveData.toneAndManner || '친근함',
|
||||
emotionIntensity: saveData.emotionIntensity || '보통',
|
||||
eventName: saveData.eventName || '',
|
||||
startDate: saveData.startDate,
|
||||
endDate: saveData.endDate,
|
||||
photoStyle: saveData.photoStyle || '밝고 화사한',
|
||||
targetAudience: saveData.targetAudience,
|
||||
promotionType: saveData.promotionType,
|
||||
imageStyle: saveData.imageStyle,
|
||||
promotionStartDate: saveData.promotionStartDate,
|
||||
promotionEndDate: saveData.promotionEndDate
|
||||
}
|
||||
|
||||
const result = await contentService.savePoster(requestData)
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ [STORE] 포스터 저장 성공')
|
||||
|
||||
// 목록 새로고침
|
||||
await loadContents()
|
||||
|
||||
return { success: true, message: '포스터가 저장되었습니다.' }
|
||||
} else {
|
||||
console.error('❌ [STORE] 포스터 저장 실패:', result.message)
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] 포스터 저장 예외:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
const fetchContentList = async (requestFilters = {}) => {
|
||||
console.log('📋 [STORE] fetchContentList 호출:', requestFilters)
|
||||
return await loadContents(requestFilters)
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 저장 통합 메서드 (기존 호환성 유지)
|
||||
*/
|
||||
const saveContent = async (type, contentData) => {
|
||||
console.log('💾 [STORE] 콘텐츠 저장 요청 (통합):', { type, contentData })
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
let result
|
||||
|
||||
// 타입에 따라 적절한 메서드 호출
|
||||
if (type === 'poster' || contentData.contentType === 'poster' || contentData.type === 'poster') {
|
||||
result = await savePoster(contentData)
|
||||
} else if (type === 'sns' || type === 'snsContent') {
|
||||
result = await saveSnsContent(contentData)
|
||||
} else {
|
||||
// 기본적으로 SNS 콘텐츠로 간주
|
||||
result = await saveSnsContent(contentData)
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, message: result.message || '콘텐츠가 저장되었습니다.' }
|
||||
} else {
|
||||
return { success: false, error: result.error }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] 통합 콘텐츠 저장 실패:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 기타 콘텐츠 관리 메서드들 =====
|
||||
/**
|
||||
* 진행 중인 콘텐츠 조회
|
||||
* fetchOngoingContents를 실제 API 호출로 수정
|
||||
*/
|
||||
const fetchOngoingContents = async (period = 'month') => {
|
||||
isLoading.value = true
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const result = await contentService.getOngoingContents(period)
|
||||
|
||||
if (result.success) {
|
||||
if (result && result.success) {
|
||||
ongoingContents.value = result.data || []
|
||||
return { success: true }
|
||||
} else {
|
||||
console.error('❌ 진행 중인 콘텐츠 조회 실패:', result.message)
|
||||
return { success: false, error: result.message }
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || '진행 중인 콘텐츠 조회에 실패했습니다.'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 진행 중인 콘텐츠 조회 예외:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
console.error('❌ [STORE] fetchOngoingContents 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -707,25 +458,33 @@ export const useContentStore = defineStore('content', () => {
|
||||
* 콘텐츠 상세 조회
|
||||
*/
|
||||
const fetchContentDetail = async (contentId) => {
|
||||
loading.value = true
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await contentService.getContentDetail(contentId)
|
||||
|
||||
if (result.success) {
|
||||
if (result && result.success) {
|
||||
selectedContent.value = result.data
|
||||
return result.data
|
||||
return { success: true, data: result.data }
|
||||
} else {
|
||||
throw new Error(result.message || '콘텐츠 상세 조회에 실패했습니다.')
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || '콘텐츠 상세 조회에 실패했습니다.'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 콘텐츠 상세 조회 실패:', error)
|
||||
throw error
|
||||
console.error('❌ [STORE] fetchContentDetail 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 콘텐츠 관리 기능들 =====
|
||||
|
||||
/**
|
||||
* 콘텐츠 수정
|
||||
*/
|
||||
@ -781,80 +540,66 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 유틸리티 메서드들 =====
|
||||
/**
|
||||
* 타겟 타입을 카테고리로 매핑
|
||||
* 콘텐츠 발행
|
||||
*/
|
||||
const mapTargetToCategory = (targetType) => {
|
||||
const mapping = {
|
||||
'new_menu': '메뉴소개',
|
||||
'discount': '이벤트',
|
||||
'store': '인테리어',
|
||||
'event': '이벤트',
|
||||
'menu': '메뉴소개',
|
||||
'service': '서비스'
|
||||
}
|
||||
return mapping[targetType] || '메뉴소개'
|
||||
}
|
||||
const publishContent = async (contentId, publishData) => {
|
||||
isLoading.value = true
|
||||
|
||||
/**
|
||||
* 플랫폼별 특성 조회
|
||||
*/
|
||||
const getPlatformSpec = (platform) => {
|
||||
return PLATFORM_SPECS?.[platform] || null
|
||||
try {
|
||||
const result = await contentService.publishContent(contentId, publishData)
|
||||
|
||||
if (result && result.success) {
|
||||
// 목록 새로고침
|
||||
await fetchContentList()
|
||||
return { success: true, message: '콘텐츠가 발행되었습니다.' }
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || '발행에 실패했습니다.'
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼 유효성 검사
|
||||
*/
|
||||
const validatePlatform = (platform) => {
|
||||
return PLATFORM_SPECS ? Object.keys(PLATFORM_SPECS).includes(platform) : true
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터 설정
|
||||
*/
|
||||
const setFilters = (newFilters) => {
|
||||
filters.value = { ...filters.value, ...newFilters }
|
||||
pagination.value.page = 1 // 필터 변경 시 첫 페이지로
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] publishContent 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지네이션 설정
|
||||
*/
|
||||
const setPagination = (newPagination) => {
|
||||
pagination.value = { ...pagination.value, ...newPagination }
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
const reset = () => {
|
||||
contentList.value = []
|
||||
contents.value = []
|
||||
ongoingContents.value = []
|
||||
selectedContent.value = null
|
||||
generatedContent.value = null
|
||||
totalCount.value = 0
|
||||
|
||||
filters.value = {
|
||||
contentType: '',
|
||||
platform: '',
|
||||
period: '',
|
||||
sortBy: 'latest'
|
||||
}
|
||||
|
||||
pagination.value = {
|
||||
page: 1,
|
||||
itemsPerPage: 10
|
||||
}
|
||||
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
loading.value = false
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 고급 기능들 (추가) =====
|
||||
/**
|
||||
* 콘텐츠 통계 조회
|
||||
*/
|
||||
const fetchContentStats = async (options = {}) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const result = await contentService.getContentStats(options)
|
||||
|
||||
if (result && result.success) {
|
||||
return { success: true, data: result.data }
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: result?.message || result?.error || '통계 조회에 실패했습니다.'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [STORE] fetchContentStats 실패:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || '네트워크 오류가 발생했습니다.'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 추가된 고급 콘텐츠 관리 기능들 =====
|
||||
|
||||
/**
|
||||
* 콘텐츠 검색
|
||||
*/
|
||||
@ -881,7 +626,7 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 콘텐츠 통계 조회
|
||||
* 콘텐츠 통계 조회 (추가)
|
||||
*/
|
||||
const getContentStats = async (statsFilters = {}) => {
|
||||
loading.value = true
|
||||
@ -971,54 +716,81 @@ export const useContentStore = defineStore('content', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 유틸리티 메서드들 =====
|
||||
/**
|
||||
* 콘텐츠 템플릿 목록 조회
|
||||
* 타겟 타입을 카테고리로 매핑
|
||||
*/
|
||||
const getContentTemplates = async (type = 'all') => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const result = await contentService.getContentTemplates(type)
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
const mapTargetToCategory = (targetType) => {
|
||||
const mapping = {
|
||||
'new_menu': '메뉴소개',
|
||||
'discount': '이벤트',
|
||||
'store': '인테리어',
|
||||
'event': '이벤트',
|
||||
'menu': '메뉴소개',
|
||||
'service': '서비스'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 템플릿 목록 조회 실패:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
return mapping[targetType] || '메뉴소개'
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼별 특성 조회
|
||||
*/
|
||||
const getPlatformSpec = (platform) => {
|
||||
return PLATFORM_SPECS?.[platform] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 플랫폼 유효성 검사
|
||||
*/
|
||||
const validatePlatform = (platform) => {
|
||||
return PLATFORM_SPECS ? Object.keys(PLATFORM_SPECS).includes(platform) : true
|
||||
}
|
||||
|
||||
/**
|
||||
* 필터 설정
|
||||
*/
|
||||
const setFilters = (newFilters) => {
|
||||
filters.value = { ...filters.value, ...newFilters }
|
||||
pagination.value.page = 1 // 필터 변경 시 첫 페이지로
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지네이션 설정
|
||||
*/
|
||||
const setPagination = (newPagination) => {
|
||||
pagination.value = { ...pagination.value, ...newPagination }
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화
|
||||
*/
|
||||
const resetState = () => {
|
||||
contentList.value = []
|
||||
contents.value = []
|
||||
ongoingContents.value = []
|
||||
selectedContent.value = null
|
||||
generatedContent.value = null
|
||||
totalCount.value = 0
|
||||
|
||||
filters.value = {
|
||||
contentType: '',
|
||||
platform: '',
|
||||
period: '',
|
||||
sortBy: 'latest'
|
||||
}
|
||||
|
||||
pagination.value = {
|
||||
page: 1,
|
||||
itemsPerPage: 10
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿으로 콘텐츠 생성
|
||||
*/
|
||||
const generateFromTemplate = async (templateId, customData = {}) => {
|
||||
generating.value = true
|
||||
|
||||
try {
|
||||
const result = await contentService.generateFromTemplate(templateId, customData)
|
||||
|
||||
if (result.success) {
|
||||
generatedContent.value = result.data
|
||||
return { success: true, data: result.data }
|
||||
} else {
|
||||
return { success: false, error: result.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 템플릿 콘텐츠 생성 실패:', error)
|
||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 반환할 store 객체 =====
|
||||
return {
|
||||
// 상태
|
||||
// 상태 (readonly로 보호)
|
||||
contentList: readonly(contentList),
|
||||
contents: readonly(contents), // ContentManagementView에서 사용
|
||||
ongoingContents: readonly(ongoingContents),
|
||||
@ -1038,41 +810,35 @@ export const useContentStore = defineStore('content', () => {
|
||||
paginatedContents,
|
||||
totalPages,
|
||||
|
||||
// 콘텐츠 목록 조회
|
||||
// 기본 CRUD 액션들
|
||||
loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함)
|
||||
generateContent,
|
||||
saveContent,
|
||||
savePoster, // 포스터 전용 저장
|
||||
saveSnsContent, // SNS 콘텐츠 전용 저장
|
||||
fetchContentList, // 기존 호환성 유지
|
||||
|
||||
// AI 콘텐츠 생성
|
||||
generateContent, // 통합 메서드 (타입에 따라 라우팅)
|
||||
generateSnsContent, // SNS 전용
|
||||
generatePoster, // 포스터 전용
|
||||
|
||||
// 콘텐츠 저장
|
||||
saveContent, // 통합 메서드 (타입에 따라 라우팅)
|
||||
saveSnsContent, // SNS 전용
|
||||
savePoster, // 포스터 전용
|
||||
|
||||
// 기본 CRUD
|
||||
fetchOngoingContents,
|
||||
fetchContentDetail,
|
||||
updateContent,
|
||||
deleteContent,
|
||||
|
||||
// 추가 액션들
|
||||
publishContent,
|
||||
fetchContentStats,
|
||||
|
||||
// 고급 콘텐츠 관리 기능들
|
||||
searchContents,
|
||||
getContentStats,
|
||||
duplicateContent,
|
||||
updateContentStatus,
|
||||
toggleContentFavorite,
|
||||
|
||||
// 유틸리티
|
||||
mapTargetToCategory,
|
||||
getPlatformSpec,
|
||||
validatePlatform,
|
||||
setFilters,
|
||||
setPagination,
|
||||
reset,
|
||||
|
||||
// 고급 기능
|
||||
searchContents,
|
||||
getContentStats,
|
||||
duplicateContent,
|
||||
updateContentStatus,
|
||||
toggleContentFavorite,
|
||||
getContentTemplates,
|
||||
generateFromTemplate
|
||||
resetState
|
||||
}
|
||||
})
|
||||
@ -1,3 +1,5 @@
|
||||
//* src/views/ContentCreationView.vue - 수정된 완전한 파일
|
||||
|
||||
<template>
|
||||
<v-container fluid class="pa-0" style="height: 100vh; overflow: hidden;">
|
||||
<!-- 책자 형식 레이아웃 -->
|
||||
@ -205,7 +207,7 @@
|
||||
<!-- 요구사항 -->
|
||||
<v-textarea
|
||||
v-model="formData.requirements"
|
||||
label="구체적인 요구사항 (선택사항)"
|
||||
label="구체적인 요구사항"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
density="compact"
|
||||
@ -230,7 +232,7 @@
|
||||
multiple
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
@change="handleFileUpload"
|
||||
@update:model-value="handleFileUpload"
|
||||
density="compact"
|
||||
:rules="selectedType === 'poster' ? imageRequiredRules : []"
|
||||
/>
|
||||
@ -270,7 +272,7 @@
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
:disabled="!formValid || remainingGenerations <= 0 || contentStore.generating"
|
||||
:disabled="!canGenerate || remainingGenerations <= 0 || contentStore.generating"
|
||||
:loading="contentStore.generating"
|
||||
@click="generateContent"
|
||||
class="px-8"
|
||||
@ -386,6 +388,41 @@
|
||||
|
||||
<!-- 콘텐츠 내용 -->
|
||||
<div class="text-body-2 mb-3" style="line-height: 1.6;">
|
||||
<!-- ✅ 포스터인 경우 이미지로 표시 -->
|
||||
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
||||
<v-img
|
||||
v-if="currentVersion.posterImage || currentVersion.content"
|
||||
:src="currentVersion.posterImage || currentVersion.content"
|
||||
:alt="currentVersion.title"
|
||||
cover
|
||||
class="rounded-lg elevation-2 mb-3"
|
||||
style="max-width: 100%; max-height: 300px; aspect-ratio: 3/4;"
|
||||
@click="previewImage(currentVersion.posterImage || currentVersion.content, currentVersion.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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ SNS인 경우 기존 텍스트 표시 -->
|
||||
<div v-else>
|
||||
<div v-if="isHtmlContent(currentVersion.content)"
|
||||
class="html-content preview-content">
|
||||
<div v-html="truncateHtmlContent(currentVersion.content, 200)"></div>
|
||||
@ -395,6 +432,7 @@
|
||||
</div>
|
||||
<div v-else>{{ truncateText(currentVersion.content, 150) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 해시태그 -->
|
||||
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-3">
|
||||
@ -466,9 +504,51 @@
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-4" style="max-height: 500px;">
|
||||
<!-- 전체 콘텐츠 -->
|
||||
<!-- ✅ 포스터인 경우 이미지 표시, SNS인 경우 텍스트 표시 -->
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-2">콘텐츠</h4>
|
||||
|
||||
<!-- ✅ 포스터인 경우 이미지로 표시 -->
|
||||
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
|
||||
<v-img
|
||||
v-if="currentVersion.posterImage || currentVersion.content"
|
||||
:src="currentVersion.posterImage || currentVersion.content"
|
||||
:alt="currentVersion.title"
|
||||
cover
|
||||
class="rounded-lg elevation-2"
|
||||
style="max-width: 400px; aspect-ratio: 3/4; cursor: pointer;"
|
||||
@click="previewImage(currentVersion.posterImage || currentVersion.content, currentVersion.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: 200px;">
|
||||
{{ (currentVersion.posterImage || currentVersion.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="currentVersion.posterImage || currentVersion.content">
|
||||
URL: {{ currentVersion.posterImage || currentVersion.content }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ SNS인 경우 기존 텍스트 표시 -->
|
||||
<div v-else>
|
||||
<div v-if="isHtmlContent(currentVersion.content)"
|
||||
class="pa-3 bg-grey-lighten-5 rounded html-content"
|
||||
style="line-height: 1.6;"
|
||||
@ -480,6 +560,7 @@
|
||||
{{ currentVersion.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 해시태그 -->
|
||||
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-4">
|
||||
@ -583,12 +664,12 @@ const router = useRouter()
|
||||
const contentStore = useContentStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 반응형 데이터
|
||||
// ✅ 반응형 데이터 - isGenerating 추가
|
||||
const selectedType = ref('sns')
|
||||
const formValid = ref(false)
|
||||
const uploadedFiles = ref([])
|
||||
const previewImages = ref([])
|
||||
const isPublishing = ref(false)
|
||||
const isGenerating = ref(false) // ✅ 추가
|
||||
const publishingIndex = ref(-1)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedVersion = ref(0)
|
||||
@ -603,14 +684,28 @@ const formData = ref({
|
||||
eventName: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
requirements: '',
|
||||
content: '',
|
||||
hashtags: [],
|
||||
category: '기타',
|
||||
targetAge: '20대',
|
||||
promotionStartDate: '',
|
||||
promotionEndDate: ''
|
||||
promotionEndDate: '',
|
||||
requirements: '',
|
||||
toneAndManner: '친근함',
|
||||
emotionIntensity: '보통',
|
||||
imageStyle: '모던',
|
||||
promotionType: '할인 정보',
|
||||
photoStyle: '밝고 화사한'
|
||||
})
|
||||
|
||||
// AI 옵션 - 타겟 연령층만
|
||||
// AI 옵션
|
||||
const aiOptions = ref({
|
||||
targetAge: '20대'
|
||||
toneAndManner: 'friendly',
|
||||
promotion: 'general',
|
||||
emotionIntensity: 'normal',
|
||||
photoStyle: '밝고 화사한',
|
||||
imageStyle: '모던',
|
||||
targetAge: '20대',
|
||||
})
|
||||
|
||||
// 상수 정의
|
||||
@ -639,10 +734,9 @@ const platformOptions = [
|
||||
]
|
||||
|
||||
const targetTypes = [
|
||||
{ title: '신메뉴', value: 'new_menu' },
|
||||
{ title: '할인 이벤트', value: 'discount' },
|
||||
{ title: '매장 홍보', value: 'store' },
|
||||
{ title: '일반 이벤트', value: 'event' }
|
||||
{ title: '메뉴', value: 'menu' },
|
||||
{ title: '매장', value: 'store' },
|
||||
{ title: '이벤트', value: 'event' },
|
||||
]
|
||||
|
||||
// 타겟 연령층 옵션
|
||||
@ -713,7 +807,68 @@ const promotionEndDateRules = [
|
||||
}
|
||||
]
|
||||
|
||||
// Computed
|
||||
// ✅ Computed 속성들
|
||||
const formValid = computed(() => {
|
||||
// 기본 필수 필드 검증
|
||||
if (!formData.value.title || !formData.value.targetType) {
|
||||
return false
|
||||
}
|
||||
|
||||
// SNS 타입인 경우 플랫폼 필수
|
||||
if (selectedType.value === 'sns' && !formData.value.platform) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 이벤트 타입인 경우 추가 검증
|
||||
if (formData.value.targetType === 'event') {
|
||||
if (!formData.value.eventName || !formData.value.startDate || !formData.value.endDate) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 포스터 타입인 경우 추가 검증
|
||||
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 canGenerate = computed(() => {
|
||||
try {
|
||||
// 기본 조건들 확인
|
||||
if (!formValid.value) return false
|
||||
if (!selectedType.value) return false
|
||||
if (!formData.value.title) return false
|
||||
|
||||
// SNS 타입인 경우 플랫폼 필수
|
||||
if (selectedType.value === 'sns' && !formData.value.platform) return false
|
||||
|
||||
// 포스터 타입인 경우 이미지 필수 및 홍보 기간 필수
|
||||
if (selectedType.value === 'poster') {
|
||||
if (!previewImages.value || previewImages.value.length === 0) return false
|
||||
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) return false
|
||||
}
|
||||
|
||||
// 이벤트 타입인 경우 추가 조건들
|
||||
if (formData.value.targetType === 'event') {
|
||||
if (!formData.value.eventName) return false
|
||||
if (!formData.value.startDate || !formData.value.endDate) return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ canGenerate computed 에러:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const currentVersion = computed(() => {
|
||||
return generatedVersions.value[selectedVersion.value] || null
|
||||
})
|
||||
@ -725,38 +880,76 @@ const selectContentType = (type) => {
|
||||
}
|
||||
|
||||
const handleFileUpload = (files) => {
|
||||
console.log('📁 파일 업로드 시작:', files)
|
||||
console.log('📁 파일 업로드 이벤트:', files)
|
||||
|
||||
// 파일이 없는 경우 처리
|
||||
if (!files || files.length === 0) {
|
||||
console.log('파일이 선택되지 않음')
|
||||
if (!files || (Array.isArray(files) && files.length === 0)) {
|
||||
console.log('📁 파일이 없음 - 기존 이미지 유지')
|
||||
return
|
||||
}
|
||||
|
||||
// FileList를 배열로 변환
|
||||
const fileArray = Array.from(files)
|
||||
console.log('📁 변환된 파일 배열:', fileArray)
|
||||
// 파일 배열로 변환
|
||||
let fileArray = []
|
||||
if (files instanceof FileList) {
|
||||
fileArray = Array.from(files)
|
||||
} else if (Array.isArray(files)) {
|
||||
fileArray = files
|
||||
} else {
|
||||
console.warn('⚠️ 파일 형태를 인식할 수 없음:', files)
|
||||
return
|
||||
}
|
||||
|
||||
// 기존 이미지 초기화
|
||||
console.log('📁 처리할 파일 개수:', fileArray.length)
|
||||
|
||||
// 기존 이미지 완전히 초기화 (중복 방지)
|
||||
previewImages.value = []
|
||||
|
||||
// 각 파일 개별 처리
|
||||
fileArray.forEach((file, index) => {
|
||||
if (file && file.type && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
console.log(`📁 파일 ${index + 1} 읽기 완료: ${file.name}`)
|
||||
|
||||
// 중복 방지를 위해 기존에 같은 이름의 파일이 있는지 확인
|
||||
const existingIndex = previewImages.value.findIndex(img => img.name === file.name && img.size === file.size)
|
||||
|
||||
if (existingIndex === -1) {
|
||||
// 새로운 파일이면 추가
|
||||
previewImages.value.push({
|
||||
file,
|
||||
url: e.target.result
|
||||
file: file,
|
||||
url: e.target.result,
|
||||
name: file.name,
|
||||
size: file.size
|
||||
})
|
||||
console.log(`✅ 파일 추가됨: ${file.name}, 현재 총 ${previewImages.value.length}개`)
|
||||
} else {
|
||||
console.log(`⚠️ 중복 파일 무시됨: ${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = (error) => {
|
||||
console.error(`❌ 파일 ${index + 1} 읽기 실패:`, error)
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
} else {
|
||||
console.warn(`⚠️ 이미지가 아닌 파일 건너뜀: ${file?.name}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (index) => {
|
||||
console.log('🗑️ 이미지 삭제:', index)
|
||||
previewImages.value.splice(index, 1)
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
|
||||
// 업로드된 파일 목록도 업데이트
|
||||
if (uploadedFiles.value && uploadedFiles.value.length > index) {
|
||||
const newFiles = Array.from(uploadedFiles.value)
|
||||
newFiles.splice(index, 1)
|
||||
uploadedFiles.value = newFiles
|
||||
}
|
||||
}
|
||||
|
||||
const generateContent = async () => {
|
||||
@ -770,208 +963,73 @@ const generateContent = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🎯 [UI] ${selectedType.value.toUpperCase()} 콘텐츠 생성 요청 시작`)
|
||||
|
||||
// ✅ 이미지 상태 상세 로깅
|
||||
console.log('📁 [UI] 현재 이미지 상태:')
|
||||
console.log(' - previewImages.value:', previewImages.value)
|
||||
console.log(' - previewImages 타입:', typeof previewImages.value)
|
||||
console.log(' - previewImages 배열 여부:', Array.isArray(previewImages.value))
|
||||
console.log(' - previewImages 길이:', previewImages.value?.length)
|
||||
|
||||
if (previewImages.value && Array.isArray(previewImages.value)) {
|
||||
previewImages.value.forEach((img, index) => {
|
||||
console.log(` - 이미지 ${index + 1}:`, {
|
||||
exists: !!img,
|
||||
hasUrl: !!img?.url,
|
||||
urlType: typeof img?.url,
|
||||
urlLength: img?.url?.length,
|
||||
urlPreview: img?.url?.substring(0, 50)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ✅ 실제 매장 ID 가져오기
|
||||
let actualStoreId = 1
|
||||
isGenerating.value = true
|
||||
|
||||
try {
|
||||
console.log('🚀 [UI] 콘텐츠 생성 시작')
|
||||
console.log('📋 [UI] 폼 데이터:', formData.value)
|
||||
console.log('📁 [UI] 이미지 데이터:', previewImages.value)
|
||||
|
||||
// ✅ 매장 ID 가져오기
|
||||
let storeId = 1 // 기본값
|
||||
|
||||
try {
|
||||
// localStorage에서 매장 정보 조회 시도
|
||||
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||||
|
||||
if (storeInfo.storeId) {
|
||||
actualStoreId = storeInfo.storeId
|
||||
storeId = storeInfo.storeId
|
||||
} else if (userInfo.storeId) {
|
||||
actualStoreId = userInfo.storeId
|
||||
storeId = userInfo.storeId
|
||||
} else {
|
||||
console.warn('⚠️ localStorage에서 매장 ID를 찾을 수 없음, 기본값 사용:', storeId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [UI] 매장 정보 파싱 실패, 기본값 사용:', actualStoreId)
|
||||
console.warn('⚠️ 매장 정보 파싱 실패, 기본값 사용:', storeId)
|
||||
}
|
||||
|
||||
// ✅ 이미지 처리 함수 개선 (더 관대한 조건)
|
||||
const processImages = () => {
|
||||
const images = []
|
||||
console.log('🏪 [UI] 사용할 매장 ID:', storeId)
|
||||
|
||||
console.log('📁 [UI] 이미지 처리 시작...')
|
||||
// ✅ Base64 이미지 URL 추출
|
||||
const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || []
|
||||
console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls)
|
||||
|
||||
// previewImages가 존재하고 배열인지 확인
|
||||
if (!previewImages.value) {
|
||||
console.log('📁 [UI] previewImages.value가 null/undefined')
|
||||
return images
|
||||
// ✅ 포스터 타입의 경우 이미지 필수 검증
|
||||
if (selectedType.value === 'poster' && imageUrls.length === 0) {
|
||||
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
|
||||
}
|
||||
|
||||
if (!Array.isArray(previewImages.value)) {
|
||||
console.log('📁 [UI] previewImages.value가 배열이 아님:', typeof previewImages.value)
|
||||
return images
|
||||
}
|
||||
|
||||
if (previewImages.value.length === 0) {
|
||||
console.log('📁 [UI] previewImages.value가 빈 배열')
|
||||
return images
|
||||
}
|
||||
|
||||
// 각 이미지 처리 (조건 완화)
|
||||
previewImages.value.forEach((img, index) => {
|
||||
console.log(`📁 [UI] 이미지 ${index + 1} 처리:`)
|
||||
console.log(' - img 존재:', !!img)
|
||||
console.log(' - img 타입:', typeof img)
|
||||
|
||||
if (!img) {
|
||||
console.log(' - 결과: null/undefined, 건너뜀')
|
||||
return
|
||||
}
|
||||
|
||||
let imageUrl = null
|
||||
|
||||
// 이미지 URL 추출 (여러 형태 지원)
|
||||
if (typeof img === 'string') {
|
||||
// 직접 문자열인 경우
|
||||
imageUrl = img
|
||||
console.log(' - 문자열 이미지:', imageUrl.substring(0, 50))
|
||||
} else if (img.url && typeof img.url === 'string') {
|
||||
// 객체에 url 속성이 있는 경우
|
||||
imageUrl = img.url
|
||||
console.log(' - 객체 이미지 URL:', imageUrl.substring(0, 50))
|
||||
} else if (img.src && typeof img.src === 'string') {
|
||||
// 객체에 src 속성이 있는 경우
|
||||
imageUrl = img.src
|
||||
console.log(' - 객체 이미지 SRC:', imageUrl.substring(0, 50))
|
||||
} else {
|
||||
console.log(' - 알 수 없는 이미지 형태:', Object.keys(img))
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ URL 유효성 검사 (조건 완화)
|
||||
if (imageUrl && typeof imageUrl === 'string' && imageUrl.length > 20) {
|
||||
// Base64 또는 HTTP URL 확인
|
||||
if (imageUrl.startsWith('data:image/') ||
|
||||
imageUrl.startsWith('http') ||
|
||||
imageUrl.startsWith('blob:') ||
|
||||
imageUrl.startsWith('//')) {
|
||||
|
||||
images.push(imageUrl)
|
||||
console.log(` - 결과: 유효한 이미지 추가 (${Math.round(imageUrl.length / 1024)}KB)`)
|
||||
} else {
|
||||
console.log(' - 결과: 유효하지 않은 형식, 건너뜀')
|
||||
}
|
||||
} else {
|
||||
console.log(' - 결과: URL이 너무 짧거나 유효하지 않음, 건너뜀')
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`📁 [UI] 이미지 처리 완료: ${images.length}개 유효 이미지`)
|
||||
return images
|
||||
}
|
||||
|
||||
// ✅ 이미지 처리 실행
|
||||
const processedImages = processImages()
|
||||
|
||||
// ✅ 기본 contentData 객체 생성
|
||||
let contentData = {
|
||||
storeId: actualStoreId,
|
||||
title: (formData.value.title || '').trim(),
|
||||
// ✅ 콘텐츠 생성 데이터 구성
|
||||
const contentData = {
|
||||
title: formData.value.title,
|
||||
platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'),
|
||||
contentType: selectedType.value,
|
||||
type: selectedType.value,
|
||||
images: processedImages, // ✅ 처리된 이미지 배열
|
||||
category: '기타',
|
||||
platform: '',
|
||||
targetAudience: '',
|
||||
requirements: formData.value.requirements || '',
|
||||
eventName: formData.value.eventName || '',
|
||||
startDate: formData.value.startDate || '',
|
||||
endDate: formData.value.endDate || '',
|
||||
targetAge: (aiOptions.value && aiOptions.value.targetAge) ? aiOptions.value.targetAge : '20대'
|
||||
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') {
|
||||
console.log('🎯 [UI] 포스터 생성 데이터 구성')
|
||||
console.log('📁 [UI] 포스터용 이미지 체크:', {
|
||||
processedImagesCount: processedImages.length,
|
||||
previewImagesCount: previewImages.value?.length || 0,
|
||||
hasImages: processedImages.length > 0
|
||||
})
|
||||
|
||||
// ✅ 포스터 이미지 체크 (상세 로깅)
|
||||
if (processedImages.length === 0) {
|
||||
console.error('❌ [UI] 포스터 이미지 없음!')
|
||||
console.error('❌ [UI] previewImages 상태:', {
|
||||
exists: !!previewImages.value,
|
||||
isArray: Array.isArray(previewImages.value),
|
||||
length: previewImages.value?.length,
|
||||
content: previewImages.value
|
||||
})
|
||||
|
||||
appStore.showSnackbar('포스터 생성을 위해 최소 1개의 이미지를 업로드해 주세요.', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
contentData.targetAudience = formData.value.targetType || 'menu'
|
||||
contentData.category = getCategory(formData.value.targetType)
|
||||
|
||||
if (formData.value.promotionStartDate) {
|
||||
contentData.promotionStartDate = formData.value.promotionStartDate
|
||||
}
|
||||
if (formData.value.promotionEndDate) {
|
||||
contentData.promotionEndDate = formData.value.promotionEndDate
|
||||
contentData.imageStyle = formData.value.imageStyle || '모던'
|
||||
contentData.promotionType = formData.value.promotionType
|
||||
contentData.photoStyle = formData.value.photoStyle || '밝고 화사한'
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('🎯 [UI] SNS 생성 데이터 구성')
|
||||
|
||||
if (!formData.value.platform) {
|
||||
appStore.showSnackbar('플랫폼을 선택해주세요.', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
contentData.platform = formData.value.platform
|
||||
contentData.targetType = formData.value.targetType || 'new_menu'
|
||||
contentData.category = getCategory(formData.value.targetType)
|
||||
}
|
||||
|
||||
// ✅ 최종 검증 및 로깅
|
||||
console.log('📤 [UI] 최종 contentData:', {
|
||||
type: contentData.type,
|
||||
title: contentData.title,
|
||||
platform: contentData.platform,
|
||||
category: contentData.category,
|
||||
storeId: contentData.storeId,
|
||||
imageCount: contentData.images.length,
|
||||
firstImagePreview: contentData.images[0]?.substring(0, 50)
|
||||
})
|
||||
|
||||
// ✅ 필수 필드 최종 체크
|
||||
if (!contentData.title) {
|
||||
throw new Error('제목을 입력해주세요.')
|
||||
}
|
||||
|
||||
if (selectedType.value === 'sns' && !contentData.platform) {
|
||||
throw new Error('플랫폼을 선택해주세요.')
|
||||
}
|
||||
|
||||
if (selectedType.value === 'poster' && contentData.images.length === 0) {
|
||||
throw new Error('포스터 생성을 위해서는 최소 1개의 이미지가 필요합니다.')
|
||||
}
|
||||
console.log('📤 [UI] 생성 요청 데이터:', contentData)
|
||||
|
||||
// ✅ contentData 무결성 체크
|
||||
if (!contentData || typeof contentData !== 'object') {
|
||||
@ -991,10 +1049,22 @@ const generateContent = async () => {
|
||||
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
|
||||
}
|
||||
|
||||
let finalContent = generated.content || generated.data?.content || ''
|
||||
// ✅ 포스터 생성 결과 처리 개선
|
||||
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 (selectedType.value === 'sns' && contentData.images && contentData.images.length > 0) {
|
||||
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);" />
|
||||
@ -1007,15 +1077,19 @@ const generateContent = async () => {
|
||||
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 = {
|
||||
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 || [],
|
||||
uploadedImages: previewImages.value || [], // ✅ 업로드된 이미지 정보 보존
|
||||
images: imageUrls, // ✅ Base64 URL 보존
|
||||
platform: contentData.platform || 'POSTER'
|
||||
}
|
||||
|
||||
@ -1029,6 +1103,8 @@ const generateContent = async () => {
|
||||
console.error('❌ [UI] 콘텐츠 생성 실패:', error)
|
||||
console.error('❌ [UI] 에러 스택:', error.stack)
|
||||
appStore.showSnackbar(error.message || '콘텐츠 생성 중 오류가 발생했습니다.', 'error')
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1037,7 +1113,9 @@ const getCategory = (targetType) => {
|
||||
'new_menu': '메뉴소개',
|
||||
'discount': '이벤트',
|
||||
'store': '인테리어',
|
||||
'event': '이벤트'
|
||||
'event': '이벤트',
|
||||
'menu': '메뉴소개',
|
||||
'service': '서비스'
|
||||
}
|
||||
return mapping[targetType] || '기타'
|
||||
}
|
||||
@ -1053,16 +1131,140 @@ const saveVersion = async (index) => {
|
||||
try {
|
||||
const version = generatedVersions.value[index]
|
||||
|
||||
await contentStore.saveContent({
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
platform: version.platform,
|
||||
category: getCategory(version.targetType),
|
||||
eventName: version.eventName,
|
||||
eventDate: version.eventDate,
|
||||
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()
|
||||
@ -1127,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'
|
||||
}
|
||||
@ -1141,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'
|
||||
}
|
||||
@ -1155,7 +1359,8 @@ const getPlatformLabel = (platform) => {
|
||||
'INSTAGRAM': '인스타그램',
|
||||
'NAVER_BLOG': '네이버 블로그',
|
||||
'FACEBOOK': '페이스북',
|
||||
'KAKAO_STORY': '카카오스토리'
|
||||
'KAKAO_STORY': '카카오스토리',
|
||||
'POSTER': '포스터'
|
||||
}
|
||||
return labels[platform] || platform
|
||||
}
|
||||
@ -1228,6 +1433,15 @@ const truncateHtmlContent = (html, maxLength) => {
|
||||
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(() => {
|
||||
console.log('📱 콘텐츠 생성 페이지 로드됨')
|
||||
@ -1314,3 +1528,4 @@ onMounted(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -152,8 +152,22 @@
|
||||
{{ getStatusText(content.status) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- ✅ 포스터인 경우와 SNS인 경우 구분하여 미리보기 표시 -->
|
||||
<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>
|
||||
</td>
|
||||
@ -272,9 +286,61 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- ✅ 포스터인 경우 이미지로 표시, SNS인 경우 HTML 렌더링 -->
|
||||
<div class="mb-4">
|
||||
<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 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 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;
|
||||
}
|
||||
</style>
|
||||
@ -252,12 +252,7 @@
|
||||
<div v-else-if="aiRecommendation" class="ai-recommendation-content">
|
||||
<!-- 추천 제목 -->
|
||||
<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>
|
||||
|
||||
<!-- 스크롤 가능한 콘텐츠 영역 -->
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user