Merge pull request #5 from won-ktds/front

content & dashboard edit
This commit is contained in:
yyoooona 2025-06-19 20:24:05 +09:00 committed by GitHub
commit b7a698b065
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 275 additions and 165 deletions

View File

@ -333,7 +333,7 @@ class ContentService {
// ✅ API 호출 // ✅ API 호출
const response = await contentApi.post('/sns/generate', formData, { const response = await contentApi.post('/sns/generate', formData, {
timeout: 30000, timeout: 0,
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }

View File

@ -1,4 +1,4 @@
//* src/views/ContentCreationView.vue - //* src/views/ContentCreationView.vue -
<template> <template>
<v-container fluid class="pa-0" style="height: 100vh; overflow: hidden;"> <v-container fluid class="pa-0" style="height: 100vh; overflow: hidden;">
@ -112,8 +112,21 @@
</template> </template>
</v-select> </v-select>
<!-- 홍보 대상 --> <!-- 홍보 대상 선택 (SNS) / 음식명 입력 (포스터) -->
<v-text-field
v-if="selectedType === 'poster'"
v-model="formData.menuName"
label="음식명"
variant="outlined"
:rules="menuNameRules"
required
density="compact"
class="mb-3"
placeholder="예: 치킨 마요 덮밥, 딸기 라떼"
/>
<v-select <v-select
v-else
v-model="formData.targetType" v-model="formData.targetType"
:items="getTargetTypes(selectedType)" :items="getTargetTypes(selectedType)"
:label="selectedType === 'poster' ? '포스터 대상' : '홍보 대상'" :label="selectedType === 'poster' ? '포스터 대상' : '홍보 대상'"
@ -122,11 +135,40 @@
required required
density="compact" density="compact"
class="mb-3" class="mb-3"
/> @update:model-value="handleTargetTypeChange"
>
<template v-slot:item="{ props, item }">
<v-list-item
v-bind="props"
:disabled="selectedType === 'poster' && item.value !== 'menu'"
:class="{ 'v-list-item--disabled': selectedType === 'poster' && item.value !== 'menu' }"
@click="handleTargetItemClick(item.value, $event)"
>
<template v-slot:prepend>
<v-icon
:color="(selectedType === 'poster' && item.value !== 'menu') ? 'grey-lighten-2' : 'primary'"
>
mdi-target
</v-icon>
</template>
<v-list-item-title
:class="{ 'text-grey-lighten-1': selectedType === 'poster' && item.value !== 'menu' }"
>
{{ item.title }}
</v-list-item-title>
<v-list-item-subtitle
v-if="selectedType === 'poster' && item.value !== 'menu'"
class="text-caption text-grey-lighten-1"
>
현재 메뉴만 지원
</v-list-item-subtitle>
</v-list-item>
</template>
</v-select>
<!-- 이벤트명 --> <!-- 이벤트명 (SNS에서 이벤트 선택 ) -->
<v-text-field <v-text-field
v-if="formData.targetType === 'event'" v-if="selectedType === 'sns' && formData.targetType === 'event'"
v-model="formData.eventName" v-model="formData.eventName"
label="이벤트명" label="이벤트명"
variant="outlined" variant="outlined"
@ -142,26 +184,28 @@
<v-text-field <v-text-field
v-model="formData.promotionStartDate" v-model="formData.promotionStartDate"
label="홍보 시작일" label="홍보 시작일"
type="datetime-local" type="date"
variant="outlined" variant="outlined"
density="compact" density="compact"
:rules="promotionStartDateRules" :rules="promotionStartDateRules"
required
/> />
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="formData.promotionEndDate" v-model="formData.promotionEndDate"
label="홍보 종료일" label="홍보 종료일"
type="datetime-local" type="date"
variant="outlined" variant="outlined"
density="compact" density="compact"
:rules="promotionEndDateRules" :rules="promotionEndDateRules"
required
/> />
</v-col> </v-col>
</v-row> </v-row>
<!-- 이벤트 기간 (이벤트인 경우) --> <!-- 이벤트 기간 (SNS에서 이벤트인 경우) -->
<v-row v-if="formData.targetType === 'event'"> <v-row v-if="selectedType === 'sns' && formData.targetType === 'event'">
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="formData.startDate" v-model="formData.startDate"
@ -272,8 +316,8 @@
<v-btn <v-btn
color="primary" color="primary"
size="large" size="large"
:disabled="!canGenerate || remainingGenerations <= 0 || contentStore.generating" :disabled="!canGenerate || remainingGenerations <= 0 || isGenerating"
:loading="contentStore.generating" :loading="isGenerating"
@click="generateContent" @click="generateContent"
class="px-8" class="px-8"
> >
@ -388,7 +432,7 @@
<!-- 콘텐츠 내용 --> <!-- 콘텐츠 내용 -->
<div class="text-body-2 mb-3" style="line-height: 1.6;"> <div class="text-body-2 mb-3" style="line-height: 1.6;">
<!-- 포스터인 경우 이미지로 표시 --> <!-- 포스터인 경우 이미지로 표시 -->
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'"> <div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
<v-img <v-img
v-if="currentVersion.posterImage || currentVersion.content" v-if="currentVersion.posterImage || currentVersion.content"
@ -421,7 +465,7 @@
</div> </div>
</div> </div>
<!-- SNS인 경우 기존 텍스트 표시 --> <!-- SNS인 경우 기존 텍스트 표시 -->
<div v-else> <div v-else>
<div v-if="isHtmlContent(currentVersion.content)" <div v-if="isHtmlContent(currentVersion.content)"
class="html-content preview-content"> class="html-content preview-content">
@ -467,7 +511,7 @@
<v-btn <v-btn
color="primary" color="primary"
variant="outlined" variant="outlined"
@click="copyToClipboard(currentVersion.content)" @click="copyFullContent(currentVersion)"
> >
<v-icon class="mr-1">mdi-content-copy</v-icon> <v-icon class="mr-1">mdi-content-copy</v-icon>
복사 복사
@ -504,11 +548,11 @@
<v-divider /> <v-divider />
<v-card-text class="pa-4" style="max-height: 500px;"> <v-card-text class="pa-4" style="max-height: 500px;">
<!-- 포스터인 경우 이미지 표시, SNS인 경우 텍스트 표시 --> <!-- 포스터인 경우 이미지 표시, SNS인 경우 텍스트 표시 -->
<div class="mb-4"> <div class="mb-4">
<h4 class="text-h6 mb-2">콘텐츠</h4> <h4 class="text-h6 mb-2">콘텐츠</h4>
<!-- 포스터인 경우 이미지로 표시 --> <!-- 포스터인 경우 이미지로 표시 -->
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'"> <div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
<v-img <v-img
v-if="currentVersion.posterImage || currentVersion.content" v-if="currentVersion.posterImage || currentVersion.content"
@ -547,7 +591,7 @@
</div> </div>
</div> </div>
<!-- SNS인 경우 기존 텍스트 표시 --> <!-- SNS인 경우 기존 텍스트 표시 -->
<div v-else> <div v-else>
<div v-if="isHtmlContent(currentVersion.content)" <div v-if="isHtmlContent(currentVersion.content)"
class="pa-3 bg-grey-lighten-5 rounded html-content" class="pa-3 bg-grey-lighten-5 rounded html-content"
@ -594,13 +638,19 @@
<v-list-item> <v-list-item>
<v-list-item-title>홍보 대상</v-list-item-title> <v-list-item-title>홍보 대상</v-list-item-title>
<template v-slot:append> <template v-slot:append>
{{ currentVersion.targetType }} {{ currentVersion.targetType || '메뉴' }}
</template> </template>
</v-list-item> </v-list-item>
<v-list-item v-if="currentVersion.eventName"> <v-list-item v-if="currentVersion.eventName || formData.eventName">
<v-list-item-title>이벤트명</v-list-item-title> <v-list-item-title>이벤트명</v-list-item-title>
<template v-slot:append> <template v-slot:append>
{{ currentVersion.eventName }} {{ currentVersion.eventName || formData.eventName }}
</template>
</v-list-item>
<v-list-item v-if="currentVersion.menuName || formData.menuName">
<v-list-item-title>음식명</v-list-item-title>
<template v-slot:append>
{{ currentVersion.menuName || formData.menuName }}
</template> </template>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
@ -639,7 +689,7 @@
</v-dialog> </v-dialog>
<!-- 로딩 오버레이 --> <!-- 로딩 오버레이 -->
<v-overlay v-model="contentStore.generating" contained persistent class="d-flex align-center justify-center"> <v-overlay v-model="isGenerating" contained persistent class="d-flex align-center justify-center">
<div class="text-center"> <div class="text-center">
<v-progress-circular color="primary" indeterminate size="64" class="mb-4" /> <v-progress-circular color="primary" indeterminate size="64" class="mb-4" />
<h3 class="text-h6 text-white mb-2">AI가 콘텐츠를 생성 중입니다</h3> <h3 class="text-h6 text-white mb-2">AI가 콘텐츠를 생성 중입니다</h3>
@ -664,23 +714,25 @@ 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 uploadedFiles = ref([]) const uploadedFiles = ref([])
const previewImages = ref([]) const previewImages = ref([])
const isPublishing = ref(false) const isPublishing = ref(false)
const isGenerating = 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 formValid = ref(false)
// //
const formData = ref({ const formData = ref({
title: '', title: '',
platform: '', platform: '',
targetType: '', targetType: '',
menuName: '',
eventName: '', eventName: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
@ -713,7 +765,7 @@ const contentTypes = [
{ {
value: 'sns', value: 'sns',
label: 'SNS 게시물', label: 'SNS 게시물',
description: '인스타그램, 페이스북 등', description: '인스타그램, 네이버블로그 등',
icon: 'mdi-instagram', icon: 'mdi-instagram',
color: 'pink' color: 'pink'
}, },
@ -728,15 +780,13 @@ const contentTypes = [
const platformOptions = [ const platformOptions = [
{ title: '인스타그램', value: 'instagram' }, { title: '인스타그램', value: 'instagram' },
{ title: '네이버 블로그', value: 'naver_blog' }, { title: '네이버 블로그', value: 'naver_blog' }
{ title: '페이스북', value: 'facebook' },
{ title: '카카오스토리', value: 'kakao_story' }
] ]
const targetTypes = [ const targetTypes = [
{ title: '메뉴', value: 'menu' }, { title: '메뉴', value: 'menu' },
{ title: '매장', value: 'store' }, { title: '매장', value: 'store' },
{ title: '이벤트', value: 'event' }, { title: '이벤트', value: 'event' }
] ]
// //
@ -754,13 +804,34 @@ const getTargetTypes = (type) => {
if (type === 'poster') { if (type === 'poster') {
return [ return [
{ title: '메뉴', value: 'menu' }, { title: '메뉴', value: 'menu' },
{ title: '이벤트', value: 'event' },
{ title: '매장', value: 'store' }, { title: '매장', value: 'store' },
{ title: '이벤트', value: 'event' },
{ title: '서비스', value: 'service' }, { title: '서비스', value: 'service' },
{ title: '할인혜택', value: 'discount' } { title: '할인혜택', value: 'discount' }
] ]
} else { }
return targetTypes // SNS
return [
{ title: '메뉴', value: 'menu' },
{ title: '매장', value: 'store' },
{ title: '이벤트', value: 'event' }
]
}
// ( )
const handleTargetItemClick = (value, event) => {
if (selectedType.value === 'poster' && value !== 'menu') {
event.preventDefault()
event.stopPropagation()
appStore.showSnackbar('현재 포스터는 메뉴 대상만 지원됩니다.', 'warning')
return false
}
}
const handleTargetTypeChange = (value) => {
if (selectedType.value === 'poster' && value !== 'menu') {
formData.value.targetType = 'menu'
appStore.showSnackbar('현재 포스터는 메뉴 대상만 지원됩니다.', 'warning')
} }
} }
@ -778,6 +849,11 @@ const targetRules = [
v => !!v || '홍보 대상을 선택해주세요' v => !!v || '홍보 대상을 선택해주세요'
] ]
const menuNameRules = [
v => !!v || '음식명은 필수입니다',
v => (v && v.length <= 50) || '음식명은 50자 이하로 입력해주세요'
]
const eventNameRules = [ const eventNameRules = [
v => !formData.value.targetType || formData.value.targetType !== 'event' || !!v || '이벤트명은 필수입니다' v => !formData.value.targetType || formData.value.targetType !== 'event' || !!v || '이벤트명은 필수입니다'
] ]
@ -807,51 +883,18 @@ 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(() => { const canGenerate = computed(() => {
try { try {
//
if (!formValid.value) return false
if (!selectedType.value) return false if (!selectedType.value) return false
if (!formData.value.title) return false if (!formData.value.title) return false
// SNS // SNS
if (selectedType.value === 'sns' && !formData.value.platform) return false if (selectedType.value === 'sns' && !formData.value.platform) return false
// // ,
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
if (!formData.value.menuName) return false
if (!previewImages.value || previewImages.value.length === 0) return false if (!previewImages.value || previewImages.value.length === 0) return false
if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) return false if (!formData.value.promotionStartDate || !formData.value.promotionEndDate) return false
} }
@ -876,19 +919,60 @@ const currentVersion = computed(() => {
// //
const selectContentType = (type) => { const selectContentType = (type) => {
selectedType.value = type selectedType.value = type
console.log(`${type} 타입 선택됨`) console.log(`${type} 타입 선택됨 - 폼 데이터 초기화`)
// ( )
formData.value = {
title: '',
platform: '',
targetType: type === 'poster' ? 'menu' : '', //
menuName: '',
eventName: '',
startDate: '',
endDate: '',
content: '',
hashtags: [],
category: '기타',
targetAge: '20대',
promotionStartDate: '',
promotionEndDate: '',
requirements: '',
toneAndManner: '친근함',
emotionIntensity: '보통',
imageStyle: '모던',
promotionType: '할인 정보',
photoStyle: '밝고 화사한'
}
//
uploadedFiles.value = []
previewImages.value = []
// AI
aiOptions.value = {
toneAndManner: 'friendly',
promotion: 'general',
emotionIntensity: 'normal',
photoStyle: '밝고 화사한',
imageStyle: '모던',
targetAge: '20대',
}
console.log('✅ 폼 데이터 초기화 완료:', {
type: type,
targetType: formData.value.targetType,
preservedVersions: generatedVersions.value.length
})
} }
const handleFileUpload = (files) => { const handleFileUpload = (files) => {
console.log('📁 파일 업로드 이벤트:', files) console.log('📁 파일 업로드 이벤트:', files)
//
if (!files || (Array.isArray(files) && files.length === 0)) { if (!files || (Array.isArray(files) && files.length === 0)) {
console.log('📁 파일이 없음 - 기존 이미지 유지') console.log('📁 파일이 없음 - 기존 이미지 유지')
return return
} }
//
let fileArray = [] let fileArray = []
if (files instanceof FileList) { if (files instanceof FileList) {
fileArray = Array.from(files) fileArray = Array.from(files)
@ -901,10 +985,8 @@ const handleFileUpload = (files) => {
console.log('📁 처리할 파일 개수:', fileArray.length) console.log('📁 처리할 파일 개수:', fileArray.length)
// ( )
previewImages.value = [] previewImages.value = []
//
fileArray.forEach((file, index) => { fileArray.forEach((file, index) => {
if (file && file.type && file.type.startsWith('image/')) { if (file && file.type && file.type.startsWith('image/')) {
const reader = new FileReader() const reader = new FileReader()
@ -912,11 +994,9 @@ const handleFileUpload = (files) => {
reader.onload = (e) => { reader.onload = (e) => {
console.log(`📁 파일 ${index + 1} 읽기 완료: ${file.name}`) console.log(`📁 파일 ${index + 1} 읽기 완료: ${file.name}`)
//
const existingIndex = previewImages.value.findIndex(img => img.name === file.name && img.size === file.size) const existingIndex = previewImages.value.findIndex(img => img.name === file.name && img.size === file.size)
if (existingIndex === -1) { if (existingIndex === -1) {
//
previewImages.value.push({ previewImages.value.push({
file: file, file: file,
url: e.target.result, url: e.target.result,
@ -944,7 +1024,6 @@ const removeImage = (index) => {
console.log('🗑️ 이미지 삭제:', index) console.log('🗑️ 이미지 삭제:', index)
previewImages.value.splice(index, 1) previewImages.value.splice(index, 1)
//
if (uploadedFiles.value && uploadedFiles.value.length > index) { if (uploadedFiles.value && uploadedFiles.value.length > index) {
const newFiles = Array.from(uploadedFiles.value) const newFiles = Array.from(uploadedFiles.value)
newFiles.splice(index, 1) newFiles.splice(index, 1)
@ -953,7 +1032,7 @@ const removeImage = (index) => {
} }
const generateContent = async () => { const generateContent = async () => {
if (!formValid.value) { if (!canGenerate.value) {
appStore.showSnackbar('모든 필수 항목을 입력해주세요.', 'warning') appStore.showSnackbar('모든 필수 항목을 입력해주세요.', 'warning')
return return
} }
@ -963,6 +1042,13 @@ const generateContent = async () => {
return return
} }
//
if (selectedType.value === 'poster' && formData.value.targetType !== 'menu') {
appStore.showSnackbar('포스터는 메뉴 대상만 생성 가능합니다.', 'warning')
formData.value.targetType = 'menu'
return
}
isGenerating.value = true isGenerating.value = true
try { try {
@ -970,11 +1056,10 @@ const generateContent = async () => {
console.log('📋 [UI] 폼 데이터:', formData.value) console.log('📋 [UI] 폼 데이터:', formData.value)
console.log('📁 [UI] 이미지 데이터:', previewImages.value) console.log('📁 [UI] 이미지 데이터:', previewImages.value)
// ID // ID
let storeId = 1 // let storeId = 1
try { try {
// localStorage
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}') const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
@ -991,16 +1076,16 @@ const generateContent = async () => {
console.log('🏪 [UI] 사용할 매장 ID:', storeId) console.log('🏪 [UI] 사용할 매장 ID:', storeId)
// Base64 URL // Base64 URL
const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || [] const imageUrls = previewImages.value?.map(img => img.url).filter(url => url) || []
console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls) console.log('📁 [UI] 추출된 이미지 URL들:', imageUrls)
// //
if (selectedType.value === 'poster' && imageUrls.length === 0) { if (selectedType.value === 'poster' && imageUrls.length === 0) {
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.') throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
} }
// //
const contentData = { const contentData = {
title: formData.value.title, title: formData.value.title,
platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'), platform: formData.value.platform || (selectedType.value === 'poster' ? 'POSTER' : 'INSTAGRAM'),
@ -1016,22 +1101,27 @@ const generateContent = async () => {
endDate: formData.value.endDate, endDate: formData.value.endDate,
toneAndManner: formData.value.toneAndManner || '친근함', toneAndManner: formData.value.toneAndManner || '친근함',
emotionIntensity: formData.value.emotionIntensity || '보통', emotionIntensity: formData.value.emotionIntensity || '보통',
images: imageUrls, // Base64 URL images: imageUrls,
storeId: storeId // ID storeId: storeId
} }
// //
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
contentData.promotionStartDate = formData.value.promotionStartDate contentData.menuName = formData.value.menuName.trim()
contentData.promotionEndDate = formData.value.promotionEndDate contentData.targetAudience = aiOptions.value.targetAge || '20대'
contentData.imageStyle = formData.value.imageStyle || '모던' contentData.category = '메뉴소개'
contentData.promotionType = formData.value.promotionType
contentData.photoStyle = formData.value.photoStyle || '밝고 화사한' if (formData.value.promotionStartDate) {
contentData.promotionStartDate = new Date(formData.value.promotionStartDate).toISOString()
}
if (formData.value.promotionEndDate) {
contentData.promotionEndDate = new Date(formData.value.promotionEndDate).toISOString()
}
} }
console.log('📤 [UI] 생성 요청 데이터:', contentData) console.log('📤 [UI] 생성 요청 데이터:', contentData)
// contentData // contentData
if (!contentData || typeof contentData !== 'object') { if (!contentData || typeof contentData !== 'object') {
throw new Error('콘텐츠 데이터 구성에 실패했습니다.') throw new Error('콘텐츠 데이터 구성에 실패했습니다.')
} }
@ -1041,7 +1131,7 @@ const generateContent = async () => {
contentData.images = [] contentData.images = []
} }
// Store // Store
console.log('🚀 [UI] contentStore.generateContent 호출') console.log('🚀 [UI] contentStore.generateContent 호출')
const generated = await contentStore.generateContent(contentData) const generated = await contentStore.generateContent(contentData)
@ -1049,18 +1139,16 @@ const generateContent = async () => {
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.') throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
} }
// //
let finalContent = '' let finalContent = ''
let posterImageUrl = '' let posterImageUrl = ''
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
// generated.data URL
posterImageUrl = generated.data?.posterImage || generated.data?.content || generated.content || '' posterImageUrl = generated.data?.posterImage || generated.data?.content || generated.content || ''
finalContent = posterImageUrl // content URL finalContent = posterImageUrl
console.log('🖼️ [UI] 포스터 이미지 URL:', posterImageUrl) console.log('🖼️ [UI] 포스터 이미지 URL:', posterImageUrl)
} else { } else {
// SNS
finalContent = generated.content || generated.data?.content || '' finalContent = generated.content || generated.data?.content || ''
// SNS // SNS
@ -1079,18 +1167,19 @@ const generateContent = async () => {
} }
} }
// //
const newContent = { const newContent = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
...contentData, ...contentData,
content: finalContent, content: finalContent,
posterImage: posterImageUrl, // URL posterImage: posterImageUrl,
hashtags: generated.hashtags || generated.data?.hashtags || [], hashtags: generated.hashtags || generated.data?.hashtags || [],
createdAt: new Date(), createdAt: new Date(),
status: 'draft', status: 'draft',
uploadedImages: previewImages.value || [], // uploadedImages: previewImages.value || [],
images: imageUrls, // Base64 URL images: imageUrls,
platform: contentData.platform || 'POSTER' platform: contentData.platform || 'POSTER',
menuName: formData.value.menuName || ''
} }
generatedVersions.value.push(newContent) generatedVersions.value.push(newContent)
@ -1133,8 +1222,8 @@ const saveVersion = async (index) => {
console.log('💾 [UI] 저장할 버전 데이터:', version) console.log('💾 [UI] 저장할 버전 데이터:', version)
// ID // ID
let storeId = 1 // let storeId = 1
try { try {
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}') const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
@ -1153,46 +1242,38 @@ const saveVersion = async (index) => {
console.log('🏪 [UI] 사용할 매장 ID:', storeId) console.log('🏪 [UI] 사용할 매장 ID:', storeId)
// //
let imageUrls = [] let imageUrls = []
// URL
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
// 1. URL
if (version.posterImage) { if (version.posterImage) {
imageUrls.push(version.posterImage) imageUrls.push(version.posterImage)
console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage) console.log('💾 [UI] 생성된 포스터 이미지:', version.posterImage)
} }
// 2. previewImages URL
if (previewImages.value && previewImages.value.length > 0) { if (previewImages.value && previewImages.value.length > 0) {
const originalImages = previewImages.value.map(img => img.url).filter(url => url) const originalImages = previewImages.value.map(img => img.url).filter(url => url)
imageUrls = [...imageUrls, ...originalImages] imageUrls = [...imageUrls, ...originalImages]
console.log('💾 [UI] 원본 이미지들:', originalImages) console.log('💾 [UI] 원본 이미지들:', originalImages)
} }
// 3. version
if (version.uploadedImages && version.uploadedImages.length > 0) { if (version.uploadedImages && version.uploadedImages.length > 0) {
const versionImages = version.uploadedImages.map(img => img.url).filter(url => url) const versionImages = version.uploadedImages.map(img => img.url).filter(url => url)
imageUrls = [...imageUrls, ...versionImages] imageUrls = [...imageUrls, ...versionImages]
} }
// 4. version.images
if (version.images && Array.isArray(version.images) && version.images.length > 0) { if (version.images && Array.isArray(version.images) && version.images.length > 0) {
imageUrls = [...imageUrls, ...version.images] imageUrls = [...imageUrls, ...version.images]
} }
//
imageUrls = [...new Set(imageUrls)] imageUrls = [...new Set(imageUrls)]
console.log('💾 [UI] 포스터 최종 이미지 URL들:', imageUrls) console.log('💾 [UI] 포스터 최종 이미지 URL들:', imageUrls)
//
if (!imageUrls || imageUrls.length === 0) { if (!imageUrls || imageUrls.length === 0) {
throw new Error('포스터 저장을 위해 최소 1개의 이미지가 필요합니다.') throw new Error('포스터 저장을 위해 최소 1개의 이미지가 필요합니다.')
} }
} else { } else {
// SNS
if (previewImages.value && previewImages.value.length > 0) { if (previewImages.value && previewImages.value.length > 0) {
imageUrls = previewImages.value.map(img => img.url).filter(url => url) imageUrls = previewImages.value.map(img => img.url).filter(url => url)
} }
@ -1203,67 +1284,44 @@ const saveVersion = async (index) => {
console.log('💾 [UI] 최종 이미지 URL들:', imageUrls) console.log('💾 [UI] 최종 이미지 URL들:', imageUrls)
// - // -
let saveData let saveData
if (selectedType.value === 'poster') { if (selectedType.value === 'poster') {
// (PosterContentSaveRequest )
saveData = { saveData = {
// ID
storeId: storeId, storeId: storeId,
// - content URL
title: version.title, title: version.title,
content: version.posterImage || version.content, // URL content content: version.posterImage || version.content,
images: imageUrls, // images: imageUrls,
//
category: getCategory(version.targetType || formData.value.targetType), category: getCategory(version.targetType || formData.value.targetType),
requirement: formData.value.requirements || `${version.title}에 대한 포스터를 만들어주세요`, requirement: formData.value.requirements || `${version.title}에 대한 포스터를 만들어주세요`,
//
eventName: version.eventName || formData.value.eventName, eventName: version.eventName || formData.value.eventName,
startDate: formData.value.startDate, startDate: formData.value.startDate,
endDate: formData.value.endDate, endDate: formData.value.endDate,
//
photoStyle: formData.value.photoStyle || '밝고 화사한' photoStyle: formData.value.photoStyle || '밝고 화사한'
} }
} else { } else {
// SNS (SnsContentSaveRequest )
saveData = { saveData = {
// ID
storeId: storeId, storeId: storeId,
//
contentType: 'SNS', contentType: 'SNS',
platform: version.platform || formData.value.platform || 'INSTAGRAM', platform: version.platform || formData.value.platform || 'INSTAGRAM',
//
title: version.title, title: version.title,
content: version.content, content: version.content,
hashtags: version.hashtags || [], hashtags: version.hashtags || [],
images: imageUrls, images: imageUrls,
//
category: getCategory(version.targetType || formData.value.targetType), category: getCategory(version.targetType || formData.value.targetType),
requirement: formData.value.requirements || `${version.title}에 대한 SNS 게시물을 만들어주세요`, requirement: formData.value.requirements || `${version.title}에 대한 SNS 게시물을 만들어주세요`,
toneAndManner: formData.value.toneAndManner || '친근함', toneAndManner: formData.value.toneAndManner || '친근함',
emotionIntensity: formData.value.emotionIntensity || '보통', emotionIntensity: formData.value.emotionIntensity || '보통',
//
eventName: version.eventName || formData.value.eventName, eventName: version.eventName || formData.value.eventName,
startDate: formData.value.startDate, startDate: formData.value.startDate,
endDate: formData.value.endDate, endDate: formData.value.endDate,
//
status: 'PUBLISHED' status: 'PUBLISHED'
} }
} }
console.log('💾 [UI] 최종 저장 데이터:', saveData) console.log('💾 [UI] 최종 저장 데이터:', saveData)
//
await contentStore.saveContent(saveData) await contentStore.saveContent(saveData)
version.status = 'published' version.status = 'published'
@ -1297,18 +1355,32 @@ const copyToClipboard = async (content) => {
} }
} }
// - SNS
const copyFullContent = async (version) => { const copyFullContent = async (version) => {
try { try {
let fullContent = '' let fullContent = ''
if (isHtmlContent(version.content)) { //
fullContent += extractTextFromHtml(version.content) if (selectedType.value === 'poster' || version.contentType === 'poster' || version.type === 'poster') {
fullContent = version.title || '포스터'
if (formData.value.requirements) {
fullContent += '\n\n' + formData.value.requirements
}
if (version.posterImage || version.content) {
fullContent += '\n\n포스터 이미지: ' + (version.posterImage || version.content)
}
} else { } else {
fullContent += version.content // SNS HTML
} if (isHtmlContent(version.content)) {
fullContent += extractTextFromHtml(version.content)
if (version.hashtags && version.hashtags.length > 0) { } else {
fullContent += '\n\n' + version.hashtags.join(' ') fullContent += version.content || ''
}
//
if (version.hashtags && version.hashtags.length > 0) {
fullContent += '\n\n' + version.hashtags.join(' ')
}
} }
await navigator.clipboard.writeText(fullContent) await navigator.clipboard.writeText(fullContent)
@ -1352,14 +1424,8 @@ const getPlatformColor = (platform) => {
const getPlatformLabel = (platform) => { const getPlatformLabel = (platform) => {
const labels = { const labels = {
'instagram': '인스타그램',
'naver_blog': '네이버 블로그',
'facebook': '페이스북',
'kakao_story': '카카오스토리',
'INSTAGRAM': '인스타그램', 'INSTAGRAM': '인스타그램',
'NAVER_BLOG': '네이버 블로그', 'NAVER_BLOG': '네이버 블로그',
'FACEBOOK': '페이스북',
'KAKAO_STORY': '카카오스토리',
'POSTER': '포스터' 'POSTER': '포스터'
} }
return labels[platform] || platform return labels[platform] || platform
@ -1407,11 +1473,28 @@ const isHtmlContent = (content) => {
return /<[^>]+>/.test(content) return /<[^>]+>/.test(content)
} }
// HTML
const extractTextFromHtml = (html) => { const extractTextFromHtml = (html) => {
if (!html) return '' if (!html) return ''
const tempDiv = document.createElement('div')
tempDiv.innerHTML = html try {
return tempDiv.textContent || tempDiv.innerText || '' // HTML
const textContent = html
.replace(/<br\s*\/?>/gi, '\n') // <br>
.replace(/<\/p>/gi, '\n\n') // </p>
.replace(/<[^>]*>/g, '') // HTML
.replace(/&nbsp;/g, ' ') // &nbsp;
.replace(/&amp;/g, '&') // &amp; &
.replace(/&lt;/g, '<') // &lt; <
.replace(/&gt;/g, '>') // &gt; >
.replace(/&quot;/g, '"') // &quot; "
.trim()
return textContent
} catch (error) {
console.error('HTML 텍스트 추출 실패:', error)
return html
}
} }
const truncateHtmlContent = (html, maxLength) => { const truncateHtmlContent = (html, maxLength) => {
@ -1445,7 +1528,43 @@ const handleImageError = (event) => {
// //
onMounted(() => { onMounted(() => {
console.log('📱 콘텐츠 생성 페이지 로드됨') console.log('📱 콘텐츠 생성 페이지 로드됨')
//
console.log('🔍 초기 상태 확인:')
console.log('- selectedType:', selectedType.value)
console.log('- formData:', formData.value)
console.log('- previewImages:', previewImages.value)
console.log('- canGenerate 존재:', typeof canGenerate)
// 5
setTimeout(() => {
console.log('🔍 5초 후 상태:')
console.log('- formData.title:', formData.value.title)
console.log('- formData.menuName:', formData.value.menuName)
console.log('- canGenerate:', canGenerate?.value)
}, 5000)
}) })
// formData
watch(() => formData.value, (newVal) => {
console.log('📝 formData 실시간 변경:', {
title: newVal.title,
menuName: newVal.menuName,
targetType: newVal.targetType,
promotionStartDate: newVal.promotionStartDate,
promotionEndDate: newVal.promotionEndDate
})
}, { deep: true })
// canGenerate
watch(canGenerate, (newVal) => {
console.log('🎯 canGenerate 변경:', newVal)
})
// previewImages
watch(() => previewImages.value, (newVal) => {
console.log('📁 previewImages 변경:', newVal.length, '개')
}, { deep: true })
</script> </script>
<style scoped> <style scoped>
@ -1527,5 +1646,4 @@ onMounted(() => {
background: linear-gradient(transparent, white); background: linear-gradient(transparent, white);
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@ -232,14 +232,6 @@
<p class="text-caption text-grey-darken-1 mb-0">맞춤형 마케팅 제안</p> <p class="text-caption text-grey-darken-1 mb-0">맞춤형 마케팅 제안</p>
</div> </div>
</div> </div>
<v-btn
icon="mdi-refresh"
size="small"
variant="text"
color="primary"
:loading="aiLoading"
@click="refreshAiRecommendation"
/>
</div> </div>
</v-card-title> </v-card-title>