release
This commit is contained in:
parent
7be3af6560
commit
64427ef9c4
407
src/components/poster/PosterForm.vue
Normal file
407
src/components/poster/PosterForm.vue
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
//* src/components/poster/PosterForm.vue
|
||||||
|
<template>
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="handleGenerate">
|
||||||
|
<!-- 기본 정보 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-h6 mb-4">
|
||||||
|
<v-icon class="mr-2">mdi-information</v-icon>
|
||||||
|
기본 정보
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.title"
|
||||||
|
label="포스터 제목"
|
||||||
|
placeholder="예: 신메뉴 출시 이벤트"
|
||||||
|
:rules="titleRules"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="formData.targetAudience"
|
||||||
|
label="홍보 대상"
|
||||||
|
:items="targetAudienceOptions"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.requirement"
|
||||||
|
label="구체적인 요구사항"
|
||||||
|
placeholder="어떤 포스터를 원하시는지 자세히 설명해주세요"
|
||||||
|
rows="3"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 프로모션 정보 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-h6 mb-4">
|
||||||
|
<v-icon class="mr-2">mdi-calendar-range</v-icon>
|
||||||
|
프로모션 정보
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.eventName"
|
||||||
|
label="이벤트명"
|
||||||
|
placeholder="예: 신메뉴 출시 기념 이벤트"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.promotionStartDate"
|
||||||
|
label="홍보 시작일"
|
||||||
|
type="datetime-local"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="startDateRules"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.promotionEndDate"
|
||||||
|
label="홍보 종료일"
|
||||||
|
type="datetime-local"
|
||||||
|
variant="outlined"
|
||||||
|
:rules="endDateRules"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.startDate"
|
||||||
|
label="이벤트 시작일"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.endDate"
|
||||||
|
label="이벤트 종료일"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 디자인 설정 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-h6 mb-4">
|
||||||
|
<v-icon class="mr-2">mdi-palette</v-icon>
|
||||||
|
디자인 설정
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.imageStyle"
|
||||||
|
label="이미지 스타일"
|
||||||
|
:items="imageStyleOptions"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.photoStyle"
|
||||||
|
label="사진 스타일"
|
||||||
|
:items="photoStyleOptions"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.promotionType"
|
||||||
|
label="프로모션 유형"
|
||||||
|
:items="promotionTypeOptions"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.emotionIntensity"
|
||||||
|
label="감정 강도"
|
||||||
|
:items="emotionIntensityOptions"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="formData.toneAndManner"
|
||||||
|
label="톤앤매너"
|
||||||
|
:items="toneAndMannerOptions"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="formData.category"
|
||||||
|
label="카테고리"
|
||||||
|
:items="categoryOptions"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 이미지 업로드 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-h6 mb-4">
|
||||||
|
<v-icon class="mr-2">mdi-image-multiple</v-icon>
|
||||||
|
이미지 업로드
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<v-file-input
|
||||||
|
v-model="uploadedFiles"
|
||||||
|
label="이미지 선택"
|
||||||
|
placeholder="포스터에 포함할 이미지를 선택하세요"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
show-size
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
:rules="imageRules"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 업로드된 이미지 미리보기 -->
|
||||||
|
<div v-if="formData.images.length > 0" class="mb-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="(image, index) in formData.images"
|
||||||
|
:key="index"
|
||||||
|
cols="6"
|
||||||
|
sm="4"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<v-card class="elevation-2">
|
||||||
|
<v-img
|
||||||
|
:src="image"
|
||||||
|
height="120"
|
||||||
|
cover
|
||||||
|
/>
|
||||||
|
<v-card-actions class="pa-2">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
@click="removeImage(index)"
|
||||||
|
/>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
variant="elevated"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!valid || formData.images.length === 0"
|
||||||
|
@click="handleGenerate"
|
||||||
|
class="flex-grow-1"
|
||||||
|
>
|
||||||
|
<v-icon class="mr-2">mdi-magic-staff</v-icon>
|
||||||
|
AI 포스터 생성
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
size="large"
|
||||||
|
variant="elevated"
|
||||||
|
:disabled="!canSave"
|
||||||
|
@click="handleSave"
|
||||||
|
class="flex-grow-1"
|
||||||
|
>
|
||||||
|
<v-icon class="mr-2">mdi-content-save</v-icon>
|
||||||
|
포스터 저장
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 생성 폼 컴포넌트
|
||||||
|
* - 포스터 정보 입력
|
||||||
|
* - 이미지 업로드
|
||||||
|
* - 폼 유효성 검증
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Props 정의
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emits 정의
|
||||||
|
const emit = defineEmits(['update:modelValue', 'generate', 'save'])
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const form = ref(null)
|
||||||
|
const valid = ref(false)
|
||||||
|
const uploadedFiles = ref([])
|
||||||
|
|
||||||
|
// 컴퓨티드 속성
|
||||||
|
const formData = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSave = computed(() => {
|
||||||
|
return formData.value.title && formData.value.images.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 옵션 데이터
|
||||||
|
const targetAudienceOptions = [
|
||||||
|
'메뉴', '이벤트', '매장', '서비스', '할인혜택'
|
||||||
|
]
|
||||||
|
|
||||||
|
const imageStyleOptions = [
|
||||||
|
'모던', '클래식', '미니멀', '화려한', '자연스러운', '빈티지'
|
||||||
|
]
|
||||||
|
|
||||||
|
const photoStyleOptions = [
|
||||||
|
'밝고 화사한', '따뜻한 톤', '차분한 톤', '선명하고 대비가 강한', '부드러운 톤', '시크한 톤'
|
||||||
|
]
|
||||||
|
|
||||||
|
const promotionTypeOptions = [
|
||||||
|
'할인 정보', '신메뉴 소개', '이벤트 안내', '매장 홍보', '서비스 안내'
|
||||||
|
]
|
||||||
|
|
||||||
|
const emotionIntensityOptions = [
|
||||||
|
'약함', '보통', '강함', '매우 강함'
|
||||||
|
]
|
||||||
|
|
||||||
|
const toneAndMannerOptions = [
|
||||||
|
'전문적', '친근한', '활기찬', '고급스러운', '캐주얼한', '신뢰감 있는'
|
||||||
|
]
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
'이벤트', '메뉴', '할인', '공지', '홍보'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 유효성 검증 규칙
|
||||||
|
const titleRules = [
|
||||||
|
v => !!v || '제목은 필수입니다',
|
||||||
|
v => (v && v.length <= 100) || '제목은 100자 이하로 입력해주세요'
|
||||||
|
]
|
||||||
|
|
||||||
|
const startDateRules = [
|
||||||
|
v => !!v || '홍보 시작일은 필수입니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const endDateRules = [
|
||||||
|
v => !!v || '홍보 종료일은 필수입니다',
|
||||||
|
v => {
|
||||||
|
if (!v || !formData.value.promotionStartDate) return true
|
||||||
|
return new Date(v) > new Date(formData.value.promotionStartDate) || '종료일은 시작일보다 늦어야 합니다'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const imageRules = [
|
||||||
|
v => formData.value.images.length > 0 || '최소 1개의 이미지를 업로드해야 합니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 업로드 처리 - 오류 수정된 버전
|
||||||
|
*/
|
||||||
|
const handleFileUpload = (files) => {
|
||||||
|
console.log('📁 파일 업로드 시작:', files)
|
||||||
|
|
||||||
|
// 파일이 없는 경우 처리
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
console.log('파일이 선택되지 않음')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileList를 배열로 변환
|
||||||
|
const fileArray = Array.from(files)
|
||||||
|
console.log('📁 변환된 파일 배열:', fileArray)
|
||||||
|
|
||||||
|
const newImages = []
|
||||||
|
let processedCount = 0
|
||||||
|
|
||||||
|
fileArray.forEach((file, index) => {
|
||||||
|
// 이미지 파일인지 확인
|
||||||
|
if (file && file.type && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
newImages.push(e.target.result)
|
||||||
|
processedCount++
|
||||||
|
|
||||||
|
// 모든 파일 처리 완료 시
|
||||||
|
if (processedCount === fileArray.length) {
|
||||||
|
console.log('📁 모든 이미지 처리 완료:', newImages.length)
|
||||||
|
// 기존 이미지에 새 이미지 추가
|
||||||
|
formData.value.images.push(...newImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
console.error('파일 읽기 오류:', error)
|
||||||
|
processedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
} else {
|
||||||
|
console.warn('지원하지 않는 파일 형식:', file?.type)
|
||||||
|
processedCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 제거
|
||||||
|
*/
|
||||||
|
const removeImage = (index) => {
|
||||||
|
formData.value.images.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 생성 요청
|
||||||
|
*/
|
||||||
|
const handleGenerate = () => {
|
||||||
|
if (form.value?.validate()) {
|
||||||
|
emit('generate', formData.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 저장 요청
|
||||||
|
*/
|
||||||
|
const handleSave = () => {
|
||||||
|
emit('save')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시작일이 변경되면 종료일 유효성 재검증
|
||||||
|
watch(() => formData.value.promotionStartDate, () => {
|
||||||
|
if (form.value) {
|
||||||
|
form.value.validate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
318
src/components/poster/PosterPreview.vue
Normal file
318
src/components/poster/PosterPreview.vue
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
//* src/components/poster/PosterPreview.vue
|
||||||
|
<template>
|
||||||
|
<div class="poster-preview">
|
||||||
|
<!-- 로딩 상태 -->
|
||||||
|
<div v-if="loading" class="text-center py-12">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
color="primary"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<div class="text-h6">포스터 생성 중...</div>
|
||||||
|
<div class="text-body-2 text-grey-600 mt-2">AI가 멋진 포스터를 만들고 있어요</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 포스터 없음 -->
|
||||||
|
<div v-else-if="!posterData" class="text-center py-12">
|
||||||
|
<v-icon size="80" color="grey-lighten-2" class="mb-4">
|
||||||
|
mdi-image-outline
|
||||||
|
</v-icon>
|
||||||
|
<div class="text-h6 mb-2">포스터 미리보기</div>
|
||||||
|
<div class="text-body-2 text-grey-600">
|
||||||
|
좌측 폼을 작성하고 'AI 포스터 생성' 버튼을 클릭하세요
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 생성된 포스터 -->
|
||||||
|
<div v-else class="poster-result">
|
||||||
|
<!-- 포스터 이미지 -->
|
||||||
|
<div class="poster-image-container mb-4">
|
||||||
|
<v-img
|
||||||
|
:src="posterData.posterImage || '/images/placeholder-poster.jpg'"
|
||||||
|
:alt="posterData.title"
|
||||||
|
cover
|
||||||
|
class="rounded-lg elevation-4"
|
||||||
|
style="aspect-ratio: 3/4; max-height: 400px;"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<div class="d-flex align-center justify-center fill-height">
|
||||||
|
<v-progress-circular indeterminate />
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
|
||||||
|
<!-- 이미지 액션 버튼 -->
|
||||||
|
<div class="image-actions mt-3">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
@click="downloadPoster"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
다운로드
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-share-variant"
|
||||||
|
@click="sharePoster"
|
||||||
|
>
|
||||||
|
공유
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 포스터 정보 -->
|
||||||
|
<v-card variant="outlined" class="mb-4">
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
{{ 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>
|
||||||
|
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(posterData.status)"
|
||||||
|
size="small"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
{{ 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-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-for="(url, size) in posterData.posterSizes"
|
||||||
|
:key="size"
|
||||||
|
@click="viewPosterSize(size, url)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{{ size }}
|
||||||
|
</v-chip>
|
||||||
|
</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-card>
|
||||||
|
<v-card-title>
|
||||||
|
포스터 사이즈: {{ selectedSize }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<v-img
|
||||||
|
:src="selectedSizeUrl"
|
||||||
|
:alt="`포스터 ${selectedSize}`"
|
||||||
|
contain
|
||||||
|
max-height="400"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
@click="downloadSelectedSize"
|
||||||
|
>
|
||||||
|
다운로드
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click="showSizeDialog = false">
|
||||||
|
닫기
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 미리보기 컴포넌트
|
||||||
|
* - 생성된 포스터 표시
|
||||||
|
* - 다운로드 및 공유 기능
|
||||||
|
* - 다양한 사이즈 보기
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Props 정의
|
||||||
|
const props = defineProps({
|
||||||
|
posterData: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const showSizeDialog = ref(false)
|
||||||
|
const selectedSize = ref('')
|
||||||
|
const selectedSizeUrl = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 색상 반환
|
||||||
|
*/
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
'DRAFT': 'orange',
|
||||||
|
'PUBLISHED': 'success',
|
||||||
|
'ARCHIVED': 'grey'
|
||||||
|
}
|
||||||
|
return colors[status] || 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 텍스트 반환
|
||||||
|
*/
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
'DRAFT': '임시저장',
|
||||||
|
'PUBLISHED': '발행',
|
||||||
|
'ARCHIVED': '보관'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 다운로드
|
||||||
|
*/
|
||||||
|
const downloadPoster = () => {
|
||||||
|
if (!props.posterData?.posterImage) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 공유
|
||||||
|
*/
|
||||||
|
const sharePoster = async () => {
|
||||||
|
if (!props.posterData?.posterImage) return
|
||||||
|
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({
|
||||||
|
title: props.posterData.title,
|
||||||
|
text: '생성된 홍보 포스터를 확인해보세요!',
|
||||||
|
url: props.posterData.posterImage
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('공유 취소됨')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 클립보드에 URL 복사
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.posterData.posterImage)
|
||||||
|
// 성공 알림 표시 (부모 컴포넌트에서 처리)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('클립보드 복사 실패:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 사이즈 포스터 보기
|
||||||
|
*/
|
||||||
|
const viewPosterSize = (size, url) => {
|
||||||
|
selectedSize.value = size
|
||||||
|
selectedSizeUrl.value = url
|
||||||
|
showSizeDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선택된 사이즈 포스터 다운로드
|
||||||
|
*/
|
||||||
|
const downloadSelectedSize = () => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = selectedSizeUrl.value
|
||||||
|
link.download = `${props.posterData.title || '포스터'}_${selectedSize.value}.jpg`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.poster-preview {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-image-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
//* src/services/content.js - 두 파일 완전 통합 버전
|
//* src/services/content.js - 완전한 파일 (모든 수정사항 포함)
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
// runtime-env.js에서 API URL 가져오기 (대체 방식 포함)
|
// runtime-env.js에서 API URL 가져오기 (대체 방식 포함)
|
||||||
@ -106,11 +106,6 @@ class ContentService {
|
|||||||
/**
|
/**
|
||||||
* 콘텐츠 목록 조회 (CON-021: 콘텐츠 조회)
|
* 콘텐츠 목록 조회 (CON-021: 콘텐츠 조회)
|
||||||
* @param {Object} filters - 필터 조건
|
* @param {Object} filters - 필터 조건
|
||||||
* @param {string} filters.platform - 플랫폼 (instagram, blog, poster)
|
|
||||||
* @param {number} filters.storeId - 매장 ID
|
|
||||||
* @param {string} filters.contentType - 콘텐츠 타입
|
|
||||||
* @param {string} filters.period - 조회 기간
|
|
||||||
* @param {string} filters.sortBy - 정렬 기준
|
|
||||||
* @returns {Promise<Object>} 콘텐츠 목록
|
* @returns {Promise<Object>} 콘텐츠 목록
|
||||||
*/
|
*/
|
||||||
async getContents(filters = {}) {
|
async getContents(filters = {}) {
|
||||||
@ -182,7 +177,7 @@ class ContentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성)
|
* SNS 콘텐츠 생성 (CON-019: AI 콘텐츠 생성) - 수정된 버전
|
||||||
* @param {Object} contentData - 콘텐츠 생성 데이터
|
* @param {Object} contentData - 콘텐츠 생성 데이터
|
||||||
* @returns {Promise<Object>} 생성된 콘텐츠
|
* @returns {Promise<Object>} 생성된 콘텐츠
|
||||||
*/
|
*/
|
||||||
@ -190,97 +185,171 @@ class ContentService {
|
|||||||
try {
|
try {
|
||||||
console.log('🤖 SNS 콘텐츠 생성 요청:', contentData)
|
console.log('🤖 SNS 콘텐츠 생성 요청:', contentData)
|
||||||
|
|
||||||
// ✅ 이미지 처리 (SNS는 선택사항)
|
// ✅ contentData 기본 검증
|
||||||
let processedImages = []
|
if (!contentData || typeof contentData !== 'object') {
|
||||||
if (contentData.images && Array.isArray(contentData.images) && contentData.images.length > 0) {
|
throw new Error('콘텐츠 데이터가 전달되지 않았습니다.')
|
||||||
console.log('📁 [API] SNS 이미지 처리:', contentData.images.length, '개')
|
|
||||||
|
|
||||||
processedImages = contentData.images.filter(img => {
|
|
||||||
const isValid = img && typeof img === 'string' && img.length > 0
|
|
||||||
console.log('📁 [API] SNS 이미지 유효성:', { isValid, type: typeof img, length: img?.length })
|
|
||||||
return isValid
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('📁 [API] SNS 유효 이미지:', processedImages.length, '개')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 실제 전달받은 데이터만 사용 (백엔드 API 스펙에 맞춤)
|
// ✅ images 속성 보장 (방어 코드)
|
||||||
const requestData = {}
|
if (!contentData.hasOwnProperty('images')) {
|
||||||
|
console.warn('⚠️ [API] images 속성이 없음, 빈 배열로 설정')
|
||||||
|
contentData.images = []
|
||||||
|
}
|
||||||
|
|
||||||
if (contentData.storeId !== undefined) requestData.storeId = contentData.storeId
|
if (!Array.isArray(contentData.images)) {
|
||||||
|
console.warn('⚠️ [API] images가 배열이 아님, 빈 배열로 변환:', typeof contentData.images)
|
||||||
|
contentData.images = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 필수 필드 검증
|
||||||
|
const requiredFields = ['title', 'platform']
|
||||||
|
const missingFields = requiredFields.filter(field => !contentData[field])
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
throw new Error(`필수 필드가 누락되었습니다: ${missingFields.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 플랫폼 형식 통일
|
||||||
|
const normalizeplatform = (platform) => {
|
||||||
|
const platformMap = {
|
||||||
|
'INSTAGRAM': 'instagram',
|
||||||
|
'instagram': 'instagram',
|
||||||
|
'NAVER_BLOG': 'naver_blog',
|
||||||
|
'naver_blog': 'naver_blog',
|
||||||
|
'FACEBOOK': 'facebook',
|
||||||
|
'facebook': 'facebook',
|
||||||
|
'KAKAO_STORY': 'kakao_story',
|
||||||
|
'kakao_story': 'kakao_story'
|
||||||
|
}
|
||||||
|
return platformMap[platform] || platform.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 카테고리 매핑
|
||||||
|
const getCategoryFromTargetType = (targetType) => {
|
||||||
|
const categoryMap = {
|
||||||
|
'new_menu': '메뉴소개',
|
||||||
|
'menu': '메뉴소개',
|
||||||
|
'discount': '이벤트',
|
||||||
|
'event': '이벤트',
|
||||||
|
'store': '매장홍보',
|
||||||
|
'service': '서비스',
|
||||||
|
'interior': '인테리어',
|
||||||
|
'daily': '일상',
|
||||||
|
'review': '고객후기'
|
||||||
|
}
|
||||||
|
return categoryMap[targetType] || '기타'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 요청 데이터 구성
|
||||||
|
const requestData = {
|
||||||
|
// 필수 필드들
|
||||||
|
title: contentData.title.trim(),
|
||||||
|
platform: normalizeplatform(contentData.platform),
|
||||||
|
contentType: contentData.contentType || 'sns',
|
||||||
|
category: contentData.category || getCategoryFromTargetType(contentData.targetType),
|
||||||
|
images: contentData.images || [] // 기본값 보장
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ storeId 처리
|
||||||
|
if (contentData.storeId !== undefined && contentData.storeId !== null) {
|
||||||
|
requestData.storeId = contentData.storeId
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
||||||
|
requestData.storeId = storeInfo.storeId || 1
|
||||||
|
} catch {
|
||||||
|
requestData.storeId = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 선택적 필드들
|
||||||
if (contentData.storeName) requestData.storeName = contentData.storeName
|
if (contentData.storeName) requestData.storeName = contentData.storeName
|
||||||
if (contentData.storeType) requestData.storeType = contentData.storeType
|
if (contentData.storeType) requestData.storeType = contentData.storeType
|
||||||
if (contentData.platform) requestData.platform = contentData.platform
|
|
||||||
if (contentData.title) requestData.title = contentData.title
|
|
||||||
if (contentData.category) requestData.category = contentData.category
|
|
||||||
if (contentData.requirement || contentData.requirements) {
|
if (contentData.requirement || contentData.requirements) {
|
||||||
requestData.requirement = contentData.requirement || contentData.requirements
|
requestData.requirement = contentData.requirement || contentData.requirements
|
||||||
}
|
}
|
||||||
if (contentData.target || contentData.targetAudience) {
|
if (contentData.target || contentData.targetAudience) {
|
||||||
requestData.target = contentData.target || contentData.targetAudience
|
requestData.target = contentData.target || contentData.targetAudience
|
||||||
}
|
}
|
||||||
if (contentData.contentType) requestData.contentType = contentData.contentType
|
|
||||||
if (contentData.eventName) requestData.eventName = contentData.eventName
|
if (contentData.eventName) requestData.eventName = contentData.eventName
|
||||||
if (contentData.startDate) requestData.startDate = contentData.startDate
|
if (contentData.startDate) requestData.startDate = contentData.startDate
|
||||||
if (contentData.endDate) requestData.endDate = contentData.endDate
|
if (contentData.endDate) requestData.endDate = contentData.endDate
|
||||||
if (contentData.photoStyle) requestData.photoStyle = contentData.photoStyle
|
|
||||||
if (contentData.targetAge) requestData.targetAge = contentData.targetAge
|
if (contentData.targetAge) requestData.targetAge = contentData.targetAge
|
||||||
if (contentData.toneAndManner) requestData.toneAndManner = contentData.toneAndManner
|
|
||||||
if (contentData.emotionalIntensity || contentData.emotionIntensity) {
|
// ✅ 이미지 처리 (contentData.images가 보장됨)
|
||||||
requestData.emotionalIntensity = contentData.emotionalIntensity || contentData.emotionIntensity
|
console.log('📁 [API] 이미지 처리 시작:', contentData.images.length, '개')
|
||||||
|
|
||||||
|
const processedImages = contentData.images
|
||||||
|
.filter(img => img && typeof img === 'string' && img.length > 50)
|
||||||
|
.map(img => {
|
||||||
|
if (typeof img === 'string' && img.startsWith('data:image/')) {
|
||||||
|
return img // Base64 그대로 사용
|
||||||
|
} else if (typeof img === 'string' && (img.startsWith('http') || img.startsWith('//'))) {
|
||||||
|
return img // URL 그대로 사용
|
||||||
|
} else {
|
||||||
|
console.warn('📁 [API] 알 수 없는 이미지 형식:', img.substring(0, 50))
|
||||||
|
return img
|
||||||
}
|
}
|
||||||
if (contentData.promotionalType || contentData.promotionType) {
|
|
||||||
requestData.promotionalType = contentData.promotionalType || contentData.promotionType
|
|
||||||
}
|
|
||||||
if (contentData.eventDate) requestData.eventDate = contentData.eventDate
|
|
||||||
if (contentData.hashtagStyle) requestData.hashtagStyle = contentData.hashtagStyle
|
|
||||||
if (contentData.hashtagCount) requestData.hashtagCount = contentData.hashtagCount
|
|
||||||
if (contentData.contentLength) requestData.contentLength = contentData.contentLength
|
|
||||||
|
|
||||||
// 이미지는 처리된 것으로 설정
|
|
||||||
requestData.images = processedImages
|
|
||||||
|
|
||||||
// Boolean 필드들 (기본값 처리)
|
|
||||||
if (contentData.includeHashtags !== undefined) requestData.includeHashtags = contentData.includeHashtags
|
|
||||||
if (contentData.includeEmojis !== undefined) requestData.includeEmojis = contentData.includeEmojis
|
|
||||||
if (contentData.includeEmoji !== undefined) requestData.includeEmoji = contentData.includeEmoji
|
|
||||||
if (contentData.includeCallToAction !== undefined) requestData.includeCallToAction = contentData.includeCallToAction
|
|
||||||
if (contentData.includeLocation !== undefined) requestData.includeLocation = contentData.includeLocation
|
|
||||||
if (contentData.forInstagramStory !== undefined) requestData.forInstagramStory = contentData.forInstagramStory
|
|
||||||
if (contentData.forNaverBlogPost !== undefined) requestData.forNaverBlogPost = contentData.forNaverBlogPost
|
|
||||||
|
|
||||||
if (contentData.alternativeTitleCount !== undefined) requestData.alternativeTitleCount = contentData.alternativeTitleCount
|
|
||||||
if (contentData.alternativeHashtagSetCount !== undefined) requestData.alternativeHashtagSetCount = contentData.alternativeHashtagSetCount
|
|
||||||
if (contentData.preferredAiModel) requestData.preferredAiModel = contentData.preferredAiModel
|
|
||||||
|
|
||||||
console.log('📝 [API] SNS 요청 데이터:', {
|
|
||||||
...requestData,
|
|
||||||
images: `${requestData.images.length}개 이미지`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 기본 유효성 검사
|
requestData.images = processedImages
|
||||||
if (!requestData.platform) {
|
console.log('📁 [API] 처리된 이미지:', processedImages.length, '개')
|
||||||
throw new Error('플랫폼은 필수입니다.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requestData.title) {
|
// ✅ 최종 검증
|
||||||
throw new Error('제목은 필수입니다.')
|
console.log('📝 [API] 최종 SNS 요청 데이터:', {
|
||||||
|
title: requestData.title,
|
||||||
|
platform: requestData.platform,
|
||||||
|
category: requestData.category,
|
||||||
|
contentType: requestData.contentType,
|
||||||
|
storeId: requestData.storeId,
|
||||||
|
imageCount: requestData.images.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Python AI 서비스 필수 필드 검증
|
||||||
|
const pythonRequiredFields = ['title', 'category', 'contentType', 'platform', 'images']
|
||||||
|
const pythonMissingFields = pythonRequiredFields.filter(field => {
|
||||||
|
if (field === 'images') {
|
||||||
|
return !Array.isArray(requestData[field])
|
||||||
|
}
|
||||||
|
return !requestData[field]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pythonMissingFields.length > 0) {
|
||||||
|
console.error('❌ [API] Python AI 서비스 필수 필드 누락:', pythonMissingFields)
|
||||||
|
throw new Error(`AI 서비스 필수 필드가 누락되었습니다: ${pythonMissingFields.join(', ')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await contentApi.post('/sns/generate', requestData, {
|
const response = await contentApi.post('/sns/generate', requestData, {
|
||||||
timeout: 30000 // 30초
|
timeout: 30000
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data)
|
console.log('✅ [API] SNS 콘텐츠 생성 응답:', response.data)
|
||||||
return formatSuccessResponse(response.data, 'SNS 게시물이 생성되었습니다.')
|
return formatSuccessResponse(response.data, 'SNS 게시물이 생성되었습니다.')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [API] SNS 콘텐츠 생성 실패:', error)
|
console.error('❌ [API] SNS 콘텐츠 생성 실패:', error)
|
||||||
|
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
const backendMessage = error.response.data?.message || '요청 데이터가 잘못되었습니다.'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: backendMessage,
|
||||||
|
error: error.response.data
|
||||||
|
}
|
||||||
|
} else if (error.response?.status === 500) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'AI 서비스에서 콘텐츠 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||||
|
error: error.response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 포스터 생성 (CON-020: AI 포스터 생성) - 이미지 처리 강화 및 상세 검증
|
* 포스터 생성 (CON-020: AI 포스터 생성) - 수정된 버전
|
||||||
* @param {Object} posterData - 포스터 생성 데이터
|
* @param {Object} posterData - 포스터 생성 데이터
|
||||||
* @returns {Promise<Object>} 생성된 포스터
|
* @returns {Promise<Object>} 생성된 포스터
|
||||||
*/
|
*/
|
||||||
@ -532,15 +601,6 @@ class ContentService {
|
|||||||
backendMessage = '서버에서 포스터 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'
|
backendMessage = '서버에서 포스터 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유효성 검사 오류가 있다면 추출
|
|
||||||
if (error.response.data && error.response.data.errors) {
|
|
||||||
console.error('❌ [API] 유효성 검사 오류:', error.response.data.errors)
|
|
||||||
const validationMessages = Object.values(error.response.data.errors).flat()
|
|
||||||
if (validationMessages.length > 0) {
|
|
||||||
backendMessage = validationMessages.join(', ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: backendMessage,
|
message: backendMessage,
|
||||||
@ -565,6 +625,37 @@ class ContentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통합 콘텐츠 생성 (타입에 따라 SNS 또는 포스터 생성) - 수정된 버전
|
||||||
|
* @param {Object} contentData - 콘텐츠 생성 데이터
|
||||||
|
* @returns {Promise<Object>} 생성 결과
|
||||||
|
*/
|
||||||
|
async generateContent(contentData) {
|
||||||
|
console.log('🎯 [API] 통합 콘텐츠 생성:', contentData)
|
||||||
|
|
||||||
|
// ✅ contentData 유효성 검사 추가
|
||||||
|
if (!contentData || typeof contentData !== 'object') {
|
||||||
|
console.error('❌ [API] contentData가 유효하지 않음:', contentData)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '콘텐츠 데이터가 유효하지 않습니다.',
|
||||||
|
error: 'INVALID_CONTENT_DATA'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ images 속성 보장
|
||||||
|
if (!Array.isArray(contentData.images)) {
|
||||||
|
console.warn('⚠️ [API] images 속성이 배열이 아님, 빈 배열로 초기화:', contentData.images)
|
||||||
|
contentData.images = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentData.contentType === 'poster' || contentData.type === 'poster') {
|
||||||
|
return await this.generatePoster(contentData)
|
||||||
|
} else {
|
||||||
|
return await this.generateSnsContent(contentData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNS 콘텐츠 저장 (CON-010: SNS 게시물 저장)
|
* SNS 콘텐츠 저장 (CON-010: SNS 게시물 저장)
|
||||||
* @param {Object} saveData - 저장할 콘텐츠 데이터
|
* @param {Object} saveData - 저장할 콘텐츠 데이터
|
||||||
@ -639,21 +730,6 @@ class ContentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 통합 콘텐츠 생성 (타입에 따라 SNS 또는 포스터 생성)
|
|
||||||
* @param {Object} contentData - 콘텐츠 생성 데이터
|
|
||||||
* @returns {Promise<Object>} 생성 결과
|
|
||||||
*/
|
|
||||||
async generateContent(contentData) {
|
|
||||||
console.log('🎯 [API] 통합 콘텐츠 생성:', contentData)
|
|
||||||
|
|
||||||
if (contentData.contentType === 'poster' || contentData.type === 'poster') {
|
|
||||||
return await this.generatePoster(contentData)
|
|
||||||
} else {
|
|
||||||
return await this.generateSnsContent(contentData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행 중인 콘텐츠 조회
|
* 진행 중인 콘텐츠 조회
|
||||||
* @param {string} period - 조회 기간
|
* @param {string} period - 조회 기간
|
||||||
@ -662,7 +738,6 @@ class ContentService {
|
|||||||
async getOngoingContents(period = 'month') {
|
async getOngoingContents(period = 'month') {
|
||||||
try {
|
try {
|
||||||
const response = await contentApi.get(`/ongoing?period=${period}`)
|
const response = await contentApi.get(`/ongoing?period=${period}`)
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.')
|
return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
@ -677,7 +752,6 @@ class ContentService {
|
|||||||
async getContentDetail(contentId) {
|
async getContentDetail(contentId) {
|
||||||
try {
|
try {
|
||||||
const response = await contentApi.get(`/${contentId}`)
|
const response = await contentApi.get(`/${contentId}`)
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.')
|
return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
@ -708,7 +782,6 @@ class ContentService {
|
|||||||
if (updateData.images) requestData.images = updateData.images
|
if (updateData.images) requestData.images = updateData.images
|
||||||
|
|
||||||
const response = await contentApi.put(`/${contentId}`, requestData)
|
const response = await contentApi.put(`/${contentId}`, requestData)
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.')
|
return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
@ -723,7 +796,6 @@ class ContentService {
|
|||||||
async deleteContent(contentId) {
|
async deleteContent(contentId) {
|
||||||
try {
|
try {
|
||||||
await contentApi.delete(`/${contentId}`)
|
await contentApi.delete(`/${contentId}`)
|
||||||
|
|
||||||
return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.')
|
return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
@ -866,6 +938,19 @@ class ContentService {
|
|||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 저장 (통합)
|
||||||
|
* @param {Object} saveData - 저장할 콘텐츠 데이터
|
||||||
|
* @returns {Promise<Object>} 저장 결과
|
||||||
|
*/
|
||||||
|
async saveContent(saveData) {
|
||||||
|
if (saveData.contentType === 'poster' || saveData.type === 'poster') {
|
||||||
|
return await this.savePoster(saveData)
|
||||||
|
} else {
|
||||||
|
return await this.saveSnsContent(saveData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 서비스 인스턴스 생성 및 내보내기
|
// 서비스 인스턴스 생성 및 내보내기
|
||||||
|
|||||||
281
src/store/poster.js
Normal file
281
src/store/poster.js
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
//* src/store/poster.js - 이미지 처리 강화 및 디버깅 추가
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { contentService } from '@/services/content'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
|
||||||
|
export const usePosterStore = defineStore('poster', {
|
||||||
|
state: () => ({
|
||||||
|
posters: [],
|
||||||
|
currentPoster: null,
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
getPosterById: (state) => (id) => {
|
||||||
|
return state.posters.find(poster => poster.id === id)
|
||||||
|
},
|
||||||
|
|
||||||
|
recentPosters: (state) => {
|
||||||
|
return state.posters
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||||
|
.slice(0, 10)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 포스터 생성 - 이미지 처리 강화
|
||||||
|
*/
|
||||||
|
async generatePoster(posterData) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🎯 [POSTER_STORE] 포스터 생성 요청 받음:', posterData)
|
||||||
|
console.log('📁 [POSTER_STORE] 이미지 상태 확인:', {
|
||||||
|
hasImages: !!posterData.images,
|
||||||
|
isArray: Array.isArray(posterData.images),
|
||||||
|
imageCount: posterData.images?.length || 0,
|
||||||
|
imageDetails: posterData.images?.map((img, idx) => ({
|
||||||
|
index: idx,
|
||||||
|
type: typeof img,
|
||||||
|
length: img?.length,
|
||||||
|
isBase64: typeof img === 'string' && img.startsWith('data:image/'),
|
||||||
|
preview: typeof img === 'string' ? img.substring(0, 30) + '...' : 'not string'
|
||||||
|
})) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ 이미지 전처리 및 검증
|
||||||
|
let processedImages = []
|
||||||
|
if (posterData.images && Array.isArray(posterData.images) && posterData.images.length > 0) {
|
||||||
|
console.log('📁 [POSTER_STORE] 이미지 전처리 시작...')
|
||||||
|
|
||||||
|
processedImages = posterData.images
|
||||||
|
.filter((img, index) => {
|
||||||
|
const isValid = img &&
|
||||||
|
typeof img === 'string' &&
|
||||||
|
img.length > 100 &&
|
||||||
|
(img.startsWith('data:image/') || img.startsWith('http'))
|
||||||
|
|
||||||
|
console.log(`📁 [POSTER_STORE] 이미지 ${index + 1} 검증:`, {
|
||||||
|
isValid,
|
||||||
|
type: typeof img,
|
||||||
|
length: img?.length,
|
||||||
|
format: img?.substring(0, 20) || 'unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('📁 [POSTER_STORE] 전처리 결과:', {
|
||||||
|
원본: posterData.images.length,
|
||||||
|
유효: processedImages.length,
|
||||||
|
제거됨: posterData.images.length - processedImages.length
|
||||||
|
})
|
||||||
|
|
||||||
|
if (processedImages.length === 0) {
|
||||||
|
throw new Error('유효한 이미지가 없습니다. 이미지를 다시 업로드해 주세요.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ [POSTER_STORE] 이미지가 없습니다!')
|
||||||
|
throw new Error('포스터 생성을 위해 최소 1개의 이미지가 필요합니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ API 요청에 맞는 형태로 데이터 변환 - 검증된 이미지 사용
|
||||||
|
const requestData = {
|
||||||
|
storeId: posterData.storeId,
|
||||||
|
title: posterData.title,
|
||||||
|
targetAudience: posterData.targetAudience,
|
||||||
|
promotionStartDate: posterData.promotionStartDate,
|
||||||
|
promotionEndDate: posterData.promotionEndDate,
|
||||||
|
images: processedImages, // 검증된 이미지만 사용
|
||||||
|
targetAge: posterData.targetAge
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택적 필드들
|
||||||
|
if (posterData.eventName) requestData.eventName = posterData.eventName
|
||||||
|
if (posterData.imageStyle) requestData.imageStyle = posterData.imageStyle
|
||||||
|
if (posterData.promotionType) requestData.promotionType = posterData.promotionType
|
||||||
|
if (posterData.emotionIntensity) requestData.emotionIntensity = posterData.emotionIntensity
|
||||||
|
if (posterData.category) requestData.category = posterData.category
|
||||||
|
if (posterData.requirement) requestData.requirement = posterData.requirement
|
||||||
|
if (posterData.toneAndManner) requestData.toneAndManner = posterData.toneAndManner
|
||||||
|
if (posterData.startDate) requestData.startDate = posterData.startDate
|
||||||
|
if (posterData.endDate) requestData.endDate = posterData.endDate
|
||||||
|
if (posterData.photoStyle) requestData.photoStyle = posterData.photoStyle
|
||||||
|
|
||||||
|
console.log('📤 [POSTER_STORE] 최종 요청 데이터:', {
|
||||||
|
...requestData,
|
||||||
|
images: `${requestData.images.length}개 이미지 (${Math.round(JSON.stringify(requestData.images).length / 1024)}KB)`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ 마지막 검증
|
||||||
|
if (!requestData.title) {
|
||||||
|
throw new Error('제목은 필수입니다.')
|
||||||
|
}
|
||||||
|
if (!requestData.targetAudience) {
|
||||||
|
throw new Error('홍보 대상은 필수입니다.')
|
||||||
|
}
|
||||||
|
if (!requestData.images || requestData.images.length === 0) {
|
||||||
|
throw new Error('이미지는 필수입니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 [POSTER_STORE] contentService.generatePoster 호출...')
|
||||||
|
const result = await contentService.generatePoster(requestData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('✅ [POSTER_STORE] 포스터 생성 성공:', result.data)
|
||||||
|
this.currentPoster = result.data
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 생성 실패:', result.message)
|
||||||
|
this.error = result.message
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 생성 예외:', error)
|
||||||
|
|
||||||
|
// 상세한 오류 정보 로깅
|
||||||
|
if (error.response) {
|
||||||
|
console.error('❌ [POSTER_STORE] HTTP 오류:', {
|
||||||
|
status: error.response.status,
|
||||||
|
statusText: error.response.statusText,
|
||||||
|
data: error.response.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.error = error.message || '포스터 생성 중 오류가 발생했습니다.'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: this.error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 저장
|
||||||
|
*/
|
||||||
|
async savePoster(saveData) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('💾 [POSTER_STORE] 포스터 저장 요청:', saveData)
|
||||||
|
|
||||||
|
const result = await contentService.savePoster(saveData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('✅ [POSTER_STORE] 포스터 저장 성공')
|
||||||
|
// 저장된 포스터를 목록에 추가
|
||||||
|
if (result.data) {
|
||||||
|
this.posters.unshift(result.data)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 저장 실패:', result.message)
|
||||||
|
this.error = result.message
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 저장 예외:', error)
|
||||||
|
this.error = error.message || '포스터 저장 중 오류가 발생했습니다.'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: this.error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 목록 조회
|
||||||
|
*/
|
||||||
|
async fetchPosters() {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await contentService.getContents({
|
||||||
|
contentType: 'poster',
|
||||||
|
sortBy: 'latest'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.posters = result.data || []
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
this.error = result.message
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 목록 조회 예외:', error)
|
||||||
|
this.error = error.message || '포스터 목록 조회 중 오류가 발생했습니다.'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: this.error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 삭제
|
||||||
|
*/
|
||||||
|
async deletePoster(posterId) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await contentService.deleteContent(posterId)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 목록에서 삭제
|
||||||
|
this.posters = this.posters.filter(poster => poster.id !== posterId)
|
||||||
|
|
||||||
|
// 현재 포스터가 삭제된 포스터라면 초기화
|
||||||
|
if (this.currentPoster?.id === posterId) {
|
||||||
|
this.currentPoster = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
this.error = result.message
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [POSTER_STORE] 포스터 삭제 예외:', error)
|
||||||
|
this.error = error.message || '포스터 삭제 중 오류가 발생했습니다.'
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: this.error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 상태 초기화
|
||||||
|
*/
|
||||||
|
clearError() {
|
||||||
|
this.error = null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 포스터 설정
|
||||||
|
*/
|
||||||
|
setCurrentPoster(poster) {
|
||||||
|
this.currentPoster = poster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,4 +1,3 @@
|
|||||||
//* 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;">
|
||||||
<!-- 책자 형식 레이아웃 -->
|
<!-- 책자 형식 레이아웃 -->
|
||||||
@ -69,7 +68,7 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- 콘텐츠 생성 폼 -->
|
<!-- 콘텐츠 생성 폼 - SNS와 포스터 통합 -->
|
||||||
<div v-if="selectedType">
|
<div v-if="selectedType">
|
||||||
<!-- 기본 정보 -->
|
<!-- 기본 정보 -->
|
||||||
<v-card class="mb-4" elevation="1">
|
<v-card class="mb-4" elevation="1">
|
||||||
@ -79,17 +78,18 @@
|
|||||||
<!-- 제목 -->
|
<!-- 제목 -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.title"
|
v-model="formData.title"
|
||||||
label="제목"
|
:label="selectedType === 'poster' ? '포스터 제목' : '게시물 제목'"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:rules="titleRules"
|
:rules="titleRules"
|
||||||
required
|
required
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
placeholder="예: 신메뉴 출시 이벤트"
|
:placeholder="selectedType === 'poster' ? '예: 신메뉴 출시 이벤트' : '예: 맛있는 신메뉴 소개'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 플랫폼 선택 -->
|
<!-- 플랫폼 선택 (SNS만) -->
|
||||||
<v-select
|
<v-select
|
||||||
|
v-if="selectedType === 'sns'"
|
||||||
v-model="formData.platform"
|
v-model="formData.platform"
|
||||||
:items="platformOptions"
|
:items="platformOptions"
|
||||||
label="발행 플랫폼"
|
label="발행 플랫폼"
|
||||||
@ -113,8 +113,8 @@
|
|||||||
<!-- 홍보 대상 -->
|
<!-- 홍보 대상 -->
|
||||||
<v-select
|
<v-select
|
||||||
v-model="formData.targetType"
|
v-model="formData.targetType"
|
||||||
:items="targetTypes"
|
:items="getTargetTypes(selectedType)"
|
||||||
label="홍보 대상"
|
:label="selectedType === 'poster' ? '포스터 대상' : '홍보 대상'"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:rules="targetRules"
|
:rules="targetRules"
|
||||||
required
|
required
|
||||||
@ -122,7 +122,7 @@
|
|||||||
class="mb-3"
|
class="mb-3"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 이벤트명 (홍보 대상이 이벤트인 경우) -->
|
<!-- 이벤트명 -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="formData.targetType === 'event'"
|
v-if="formData.targetType === 'event'"
|
||||||
v-model="formData.eventName"
|
v-model="formData.eventName"
|
||||||
@ -134,6 +134,30 @@
|
|||||||
placeholder="예: 신메뉴 할인 이벤트"
|
placeholder="예: 신메뉴 할인 이벤트"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 프로모션 기간 (포스터만) -->
|
||||||
|
<v-row v-if="selectedType === 'poster'">
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.promotionStartDate"
|
||||||
|
label="홍보 시작일"
|
||||||
|
type="datetime-local"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:rules="promotionStartDateRules"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.promotionEndDate"
|
||||||
|
label="홍보 종료일"
|
||||||
|
type="datetime-local"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:rules="promotionEndDateRules"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- 이벤트 기간 (이벤트인 경우) -->
|
<!-- 이벤트 기간 (이벤트인 경우) -->
|
||||||
<v-row v-if="formData.targetType === 'event'">
|
<v-row v-if="formData.targetType === 'event'">
|
||||||
<v-col cols="6">
|
<v-col cols="6">
|
||||||
@ -161,18 +185,18 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- AI 설정 (백엔드 지원 필드만) -->
|
<!-- AI 설정 -->
|
||||||
<v-card class="mb-4" elevation="1">
|
<v-card class="mb-4" elevation="1">
|
||||||
<v-card-title class="text-h6 py-3">
|
<v-card-title class="text-h6 py-3">
|
||||||
<v-icon class="mr-2" color="primary">mdi-robot</v-icon>
|
<v-icon class="mr-2" color="primary">mdi-robot</v-icon>
|
||||||
AI 설정
|
AI 설정
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<!-- 사진 스타일 -->
|
<!-- 타겟 연령층 -->
|
||||||
<v-select
|
<v-select
|
||||||
v-model="aiOptions.photoStyle"
|
v-model="aiOptions.targetAge"
|
||||||
:items="photoStyleOptions"
|
:items="targetAgeOptions"
|
||||||
label="사진 스타일"
|
label="타겟 연령층"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
@ -185,27 +209,30 @@
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
rows="3"
|
rows="3"
|
||||||
density="compact"
|
density="compact"
|
||||||
placeholder="예: 젊은 고객층을 타겟으로 트렌디한 문구를 사용하고 싶어요"
|
:placeholder="selectedType === 'poster' ?
|
||||||
|
'예: 밝고 활기찬 분위기의 포스터를 만들어주세요' :
|
||||||
|
'예: 젊은 고객층을 타겟으로 트렌디한 문구를 사용하고 싶어요'"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- 사진 업로드 -->
|
<!-- 이미지 업로드 -->
|
||||||
<v-card class="mb-4" elevation="1">
|
<v-card class="mb-4" elevation="1">
|
||||||
<v-card-title class="text-h6 py-3">
|
<v-card-title class="text-h6 py-3">
|
||||||
<v-icon class="mr-2" color="primary">mdi-camera</v-icon>
|
<v-icon class="mr-2" color="primary">mdi-camera</v-icon>
|
||||||
사진 업로드 (선택사항)
|
{{ selectedType === 'poster' ? '포스터용 이미지 업로드' : '사진 업로드 (선택사항)' }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-file-input
|
<v-file-input
|
||||||
v-model="uploadedFiles"
|
v-model="uploadedFiles"
|
||||||
label="사진을 선택하세요"
|
:label="selectedType === 'poster' ? '포스터에 포함할 이미지를 선택하세요' : '사진을 선택하세요'"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
multiple
|
multiple
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
prepend-icon="mdi-camera"
|
prepend-icon="mdi-camera"
|
||||||
@change="handleFileUpload"
|
@change="handleFileUpload"
|
||||||
density="compact"
|
density="compact"
|
||||||
|
:rules="selectedType === 'poster' ? imageRequiredRules : []"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 미리보기 -->
|
<!-- 미리보기 -->
|
||||||
@ -248,8 +275,8 @@
|
|||||||
@click="generateContent"
|
@click="generateContent"
|
||||||
class="px-8"
|
class="px-8"
|
||||||
>
|
>
|
||||||
<v-icon class="mr-2">mdi-robot</v-icon>
|
<v-icon class="mr-2">{{ selectedType === 'poster' ? 'mdi-image' : 'mdi-robot' }}</v-icon>
|
||||||
AI로 콘텐츠 생성하기
|
{{ selectedType === 'poster' ? 'AI 포스터 생성하기' : 'AI로 콘텐츠 생성하기' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<div v-if="remainingGenerations <= 0" class="text-caption text-error mt-2">
|
<div v-if="remainingGenerations <= 0" class="text-caption text-error mt-2">
|
||||||
@ -288,7 +315,7 @@
|
|||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col
|
||||||
v-for="(version, index) in generatedVersions"
|
v-for="(version, index) in generatedVersions"
|
||||||
:key="index"
|
:key="`version-${index}`"
|
||||||
cols="12"
|
cols="12"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
@ -357,11 +384,10 @@
|
|||||||
{{ currentVersion.title }}
|
{{ currentVersion.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 콘텐츠 내용 (HTML 렌더링) -->
|
<!-- 콘텐츠 내용 -->
|
||||||
<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="isHtmlContent(currentVersion.content)"
|
<div v-if="isHtmlContent(currentVersion.content)"
|
||||||
class="html-content preview-content">
|
class="html-content preview-content">
|
||||||
<!-- 미리보기용 축약 HTML -->
|
|
||||||
<div v-html="truncateHtmlContent(currentVersion.content, 200)"></div>
|
<div v-html="truncateHtmlContent(currentVersion.content, 200)"></div>
|
||||||
<div v-if="currentVersion.content.length > 500" class="text-caption text-grey mt-2">
|
<div v-if="currentVersion.content.length > 500" class="text-caption text-grey mt-2">
|
||||||
... 더 보려면 '자세히 보기'를 클릭하세요
|
... 더 보려면 '자세히 보기'를 클릭하세요
|
||||||
@ -370,7 +396,6 @@
|
|||||||
<div v-else>{{ truncateText(currentVersion.content, 150) }}</div>
|
<div v-else>{{ truncateText(currentVersion.content, 150) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 해시태그 -->
|
<!-- 해시태그 -->
|
||||||
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-3">
|
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-3">
|
||||||
<v-chip
|
<v-chip
|
||||||
@ -441,8 +466,7 @@
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-text class="pa-4" style="max-height: 500px;">
|
<v-card-text class="pa-4" style="max-height: 500px;">
|
||||||
|
<!-- 전체 콘텐츠 -->
|
||||||
<!-- 전체 콘텐츠 (HTML 렌더링) -->
|
|
||||||
<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="isHtmlContent(currentVersion.content)"
|
<div v-if="isHtmlContent(currentVersion.content)"
|
||||||
@ -457,8 +481,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 해시태그 -->
|
<!-- 해시태그 -->
|
||||||
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-4">
|
<div v-if="currentVersion.hashtags && currentVersion.hashtags.length > 0" class="mb-4">
|
||||||
<h4 class="text-h6 mb-2">해시태그</h4>
|
<h4 class="text-h6 mb-2">해시태그</h4>
|
||||||
@ -571,7 +593,7 @@ 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) // AI 생성 가능 횟수
|
const remainingGenerations = ref(3)
|
||||||
|
|
||||||
// 폼 데이터
|
// 폼 데이터
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@ -582,11 +604,13 @@ const formData = ref({
|
|||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
requirements: '',
|
requirements: '',
|
||||||
|
promotionStartDate: '',
|
||||||
|
promotionEndDate: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// AI 옵션 (백엔드 DTO에 맞게 단순화)
|
// AI 옵션 - 타겟 연령층만
|
||||||
const aiOptions = ref({
|
const aiOptions = ref({
|
||||||
photoStyle: 'bright' // 백엔드에서 지원하는 photoStyle만 유지
|
targetAge: '20대'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 상수 정의
|
// 상수 정의
|
||||||
@ -621,15 +645,31 @@ const targetTypes = [
|
|||||||
{ title: '일반 이벤트', value: 'event' }
|
{ title: '일반 이벤트', value: 'event' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 백엔드에서 지원하지 않는 옵션들은 제거
|
// 타겟 연령층 옵션
|
||||||
const photoStyleOptions = [
|
const targetAgeOptions = [
|
||||||
{ title: '밝고 화사한', value: 'bright' },
|
{ title: '10대', value: '10대' },
|
||||||
{ title: '차분하고 세련된', value: 'calm' },
|
{ title: '20대', value: '20대' },
|
||||||
{ title: '빈티지한', value: 'vintage' },
|
{ title: '30대', value: '30대' },
|
||||||
{ title: '모던한', value: 'modern' },
|
{ title: '40대', value: '40대' },
|
||||||
{ title: '자연스러운', value: 'natural' }
|
{ title: '50대', value: '50대' },
|
||||||
|
{ title: '60대 이상', value: '60대 이상' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 타입별 타겟 옵션 함수
|
||||||
|
const getTargetTypes = (type) => {
|
||||||
|
if (type === 'poster') {
|
||||||
|
return [
|
||||||
|
{ title: '메뉴', value: 'menu' },
|
||||||
|
{ title: '이벤트', value: 'event' },
|
||||||
|
{ title: '매장', value: 'store' },
|
||||||
|
{ title: '서비스', value: 'service' },
|
||||||
|
{ title: '할인혜택', value: 'discount' }
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return targetTypes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 유효성 검사 규칙
|
// 유효성 검사 규칙
|
||||||
const titleRules = [
|
const titleRules = [
|
||||||
v => !!v || '제목은 필수입니다',
|
v => !!v || '제목은 필수입니다',
|
||||||
@ -657,6 +697,22 @@ const endDateRules = [
|
|||||||
v => !formData.value.startDate || !v || new Date(v) >= new Date(formData.value.startDate) || '종료일은 시작일보다 이후여야 합니다'
|
v => !formData.value.startDate || !v || new Date(v) >= new Date(formData.value.startDate) || '종료일은 시작일보다 이후여야 합니다'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const imageRequiredRules = [
|
||||||
|
v => selectedType.value !== 'poster' || (previewImages.value && previewImages.value.length > 0) || '포스터 생성을 위해 최소 1개의 이미지가 필요합니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const promotionStartDateRules = [
|
||||||
|
v => selectedType.value !== 'poster' || !!v || '홍보 시작일은 필수입니다'
|
||||||
|
]
|
||||||
|
|
||||||
|
const promotionEndDateRules = [
|
||||||
|
v => selectedType.value !== 'poster' || !!v || '홍보 종료일은 필수입니다',
|
||||||
|
v => {
|
||||||
|
if (selectedType.value !== 'poster' || !v || !formData.value.promotionStartDate) return true
|
||||||
|
return new Date(v) > new Date(formData.value.promotionStartDate) || '종료일은 시작일보다 늦어야 합니다'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const currentVersion = computed(() => {
|
const currentVersion = computed(() => {
|
||||||
return generatedVersions.value[selectedVersion.value] || null
|
return generatedVersions.value[selectedVersion.value] || null
|
||||||
@ -665,13 +721,27 @@ const currentVersion = computed(() => {
|
|||||||
// 메서드
|
// 메서드
|
||||||
const selectContentType = (type) => {
|
const selectContentType = (type) => {
|
||||||
selectedType.value = type
|
selectedType.value = type
|
||||||
|
console.log(`${type} 타입 선택됨`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileUpload = () => {
|
const handleFileUpload = (files) => {
|
||||||
|
console.log('📁 파일 업로드 시작:', files)
|
||||||
|
|
||||||
|
// 파일이 없는 경우 처리
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
console.log('파일이 선택되지 않음')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileList를 배열로 변환
|
||||||
|
const fileArray = Array.from(files)
|
||||||
|
console.log('📁 변환된 파일 배열:', fileArray)
|
||||||
|
|
||||||
|
// 기존 이미지 초기화
|
||||||
previewImages.value = []
|
previewImages.value = []
|
||||||
|
|
||||||
if (uploadedFiles.value && uploadedFiles.value.length > 0) {
|
fileArray.forEach((file, index) => {
|
||||||
uploadedFiles.value.forEach(file => {
|
if (file && file.type && file.type.startsWith('image/')) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
previewImages.value.push({
|
previewImages.value.push({
|
||||||
@ -680,8 +750,8 @@ const handleFileUpload = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeImage = (index) => {
|
const removeImage = (index) => {
|
||||||
@ -701,54 +771,239 @@ const generateContent = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🎯 콘텐츠 생성 요청 시작')
|
console.log(`🎯 [UI] ${selectedType.value.toUpperCase()} 콘텐츠 생성 요청 시작`)
|
||||||
|
|
||||||
// 백엔드 DTO에 맞는 데이터 구조
|
// ✅ 이미지 상태 상세 로깅
|
||||||
const contentData = {
|
console.log('📁 [UI] 현재 이미지 상태:')
|
||||||
// 기본 정보
|
console.log(' - previewImages.value:', previewImages.value)
|
||||||
title: formData.value.title,
|
console.log(' - previewImages 타입:', typeof previewImages.value)
|
||||||
platform: formData.value.platform, // 원본 플랫폼 값 그대로 전송
|
console.log(' - previewImages 배열 여부:', Array.isArray(previewImages.value))
|
||||||
targetType: formData.value.targetType,
|
console.log(' - previewImages 길이:', previewImages.value?.length)
|
||||||
|
|
||||||
// 이벤트 정보
|
if (previewImages.value && Array.isArray(previewImages.value)) {
|
||||||
eventName: formData.value.eventName,
|
previewImages.value.forEach((img, index) => {
|
||||||
startDate: formData.value.startDate,
|
console.log(` - 이미지 ${index + 1}:`, {
|
||||||
endDate: formData.value.endDate,
|
exists: !!img,
|
||||||
|
hasUrl: !!img?.url,
|
||||||
// 콘텐츠 정보
|
urlType: typeof img?.url,
|
||||||
requirements: formData.value.requirements,
|
urlLength: img?.url?.length,
|
||||||
category: getCategory(formData.value.targetType),
|
urlPreview: img?.url?.substring(0, 50)
|
||||||
|
})
|
||||||
// 미디어
|
})
|
||||||
images: previewImages.value.map(img => img.url),
|
|
||||||
|
|
||||||
// AI 옵션 (UI에서 설정된 값들)
|
|
||||||
aiOptions: aiOptions.value,
|
|
||||||
|
|
||||||
// 메타 정보
|
|
||||||
type: selectedType.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📤 전송할 데이터:', contentData)
|
// ✅ 실제 매장 ID 가져오기
|
||||||
|
let actualStoreId = 1
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storeInfo = JSON.parse(localStorage.getItem('storeInfo') || '{}')
|
||||||
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
|
||||||
|
|
||||||
|
if (storeInfo.storeId) {
|
||||||
|
actualStoreId = storeInfo.storeId
|
||||||
|
} else if (userInfo.storeId) {
|
||||||
|
actualStoreId = userInfo.storeId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ [UI] 매장 정보 파싱 실패, 기본값 사용:', actualStoreId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 이미지 처리 함수 개선 (더 관대한 조건)
|
||||||
|
const processImages = () => {
|
||||||
|
const images = []
|
||||||
|
|
||||||
|
console.log('📁 [UI] 이미지 처리 시작...')
|
||||||
|
|
||||||
|
// previewImages가 존재하고 배열인지 확인
|
||||||
|
if (!previewImages.value) {
|
||||||
|
console.log('📁 [UI] previewImages.value가 null/undefined')
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
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대'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 타입별 처리
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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개의 이미지가 필요합니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ contentData 무결성 체크
|
||||||
|
if (!contentData || typeof contentData !== 'object') {
|
||||||
|
throw new Error('콘텐츠 데이터 구성에 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(contentData.images)) {
|
||||||
|
console.error('❌ [UI] contentData.images가 배열이 아님!')
|
||||||
|
contentData.images = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Store 호출
|
||||||
|
console.log('🚀 [UI] contentStore.generateContent 호출')
|
||||||
const generated = await contentStore.generateContent(contentData)
|
const generated = await contentStore.generateContent(contentData)
|
||||||
|
|
||||||
// ✅ 핵심: AI 생성 결과에 이미지를 직접 추가
|
if (!generated || !generated.success) {
|
||||||
let finalContent = generated.content
|
throw new Error(generated?.message || '콘텐츠 생성에 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
// 업로드된 이미지가 있으면 콘텐츠 최상단에 추가
|
let finalContent = generated.content || generated.data?.content || ''
|
||||||
if (previewImages.value.length > 0) {
|
|
||||||
const imageHtml = previewImages.value.map(img =>
|
// SNS용 이미지 추가
|
||||||
|
if (selectedType.value === 'sns' && contentData.images && contentData.images.length > 0) {
|
||||||
|
const imageHtml = contentData.images.map(imageUrl =>
|
||||||
`<div style="margin-bottom: 15px; text-align: center;">
|
`<div style="margin-bottom: 15px; text-align: center;">
|
||||||
<img src="${img.url}" style="width: 100%; max-width: 400px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" />
|
<img src="${imageUrl}" style="width: 100%; max-width: 400px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" />
|
||||||
</div>`
|
</div>`
|
||||||
).join('')
|
).join('')
|
||||||
|
|
||||||
// HTML 콘텐츠인지 확인하고 이미지를 맨 앞에 추가
|
|
||||||
if (isHtmlContent(finalContent)) {
|
if (isHtmlContent(finalContent)) {
|
||||||
finalContent = imageHtml + finalContent
|
finalContent = imageHtml + finalContent
|
||||||
} else {
|
} else {
|
||||||
// 일반 텍스트면 HTML로 변환해서 이미지 추가
|
|
||||||
finalContent = imageHtml + `<div style="padding: 15px; font-family: 'Noto Sans KR', Arial, sans-serif; line-height: 1.6;">${finalContent.replace(/\n/g, '<br>')}</div>`
|
finalContent = imageHtml + `<div style="padding: 15px; font-family: 'Noto Sans KR', Arial, sans-serif; line-height: 1.6;">${finalContent.replace(/\n/g, '<br>')}</div>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -757,10 +1012,11 @@ const generateContent = async () => {
|
|||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
...contentData,
|
...contentData,
|
||||||
content: finalContent,
|
content: finalContent,
|
||||||
hashtags: generated.hashtags || [],
|
hashtags: generated.hashtags || generated.data?.hashtags || [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
uploadedImages: previewImages.value
|
uploadedImages: previewImages.value || [],
|
||||||
|
platform: contentData.platform || 'POSTER'
|
||||||
}
|
}
|
||||||
|
|
||||||
generatedVersions.value.push(newContent)
|
generatedVersions.value.push(newContent)
|
||||||
@ -768,8 +1024,10 @@ const generateContent = async () => {
|
|||||||
remainingGenerations.value--
|
remainingGenerations.value--
|
||||||
|
|
||||||
appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success')
|
appStore.showSnackbar(`콘텐츠 버전 ${generatedVersions.value.length}이 생성되었습니다!`, 'success')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 콘텐츠 생성 실패:', error)
|
console.error('❌ [UI] 콘텐츠 생성 실패:', error)
|
||||||
|
console.error('❌ [UI] 에러 스택:', error.stack)
|
||||||
appStore.showSnackbar(error.message || '콘텐츠 생성 중 오류가 발생했습니다.', 'error')
|
appStore.showSnackbar(error.message || '콘텐츠 생성 중 오류가 발생했습니다.', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -795,12 +1053,11 @@ const saveVersion = async (index) => {
|
|||||||
try {
|
try {
|
||||||
const version = generatedVersions.value[index]
|
const version = generatedVersions.value[index]
|
||||||
|
|
||||||
// 백엔드 DTO에 맞는 저장 데이터 구성
|
|
||||||
await contentStore.saveContent({
|
await contentStore.saveContent({
|
||||||
title: version.title,
|
title: version.title,
|
||||||
content: version.content,
|
content: version.content,
|
||||||
hashtags: version.hashtags,
|
hashtags: version.hashtags,
|
||||||
platform: version.platform, // 원본 플랫폼 값 그대로 전송
|
platform: version.platform,
|
||||||
category: getCategory(version.targetType),
|
category: getCategory(version.targetType),
|
||||||
eventName: version.eventName,
|
eventName: version.eventName,
|
||||||
eventDate: version.eventDate,
|
eventDate: version.eventDate,
|
||||||
@ -812,7 +1069,6 @@ const saveVersion = async (index) => {
|
|||||||
|
|
||||||
appStore.showSnackbar(`버전 ${index + 1}이 성공적으로 저장되었습니다!`, 'success')
|
appStore.showSnackbar(`버전 ${index + 1}이 성공적으로 저장되었습니다!`, 'success')
|
||||||
|
|
||||||
// 저장 후 콘텐츠 관리 페이지로 이동할지 물어보기
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (confirm('저장된 콘텐츠를 확인하시겠습니까?')) {
|
if (confirm('저장된 콘텐츠를 확인하시겠습니까?')) {
|
||||||
router.push('/content')
|
router.push('/content')
|
||||||
@ -829,7 +1085,6 @@ const saveVersion = async (index) => {
|
|||||||
|
|
||||||
const copyToClipboard = async (content) => {
|
const copyToClipboard = async (content) => {
|
||||||
try {
|
try {
|
||||||
// HTML 콘텐츠인 경우 텍스트만 추출해서 복사
|
|
||||||
const textToCopy = isHtmlContent(content) ? extractTextFromHtml(content) : content
|
const textToCopy = isHtmlContent(content) ? extractTextFromHtml(content) : content
|
||||||
|
|
||||||
await navigator.clipboard.writeText(textToCopy)
|
await navigator.clipboard.writeText(textToCopy)
|
||||||
@ -840,19 +1095,16 @@ const copyToClipboard = async (content) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전체 콘텐츠 복사 (콘텐츠 + 해시태그)
|
|
||||||
const copyFullContent = async (version) => {
|
const copyFullContent = async (version) => {
|
||||||
try {
|
try {
|
||||||
let fullContent = ''
|
let fullContent = ''
|
||||||
|
|
||||||
// 콘텐츠 추가 (HTML인 경우 텍스트만 추출)
|
|
||||||
if (isHtmlContent(version.content)) {
|
if (isHtmlContent(version.content)) {
|
||||||
fullContent += extractTextFromHtml(version.content)
|
fullContent += extractTextFromHtml(version.content)
|
||||||
} else {
|
} else {
|
||||||
fullContent += version.content
|
fullContent += version.content
|
||||||
}
|
}
|
||||||
|
|
||||||
// 해시태그 추가
|
|
||||||
if (version.hashtags && version.hashtags.length > 0) {
|
if (version.hashtags && version.hashtags.length > 0) {
|
||||||
fullContent += '\n\n' + version.hashtags.join(' ')
|
fullContent += '\n\n' + version.hashtags.join(' ')
|
||||||
}
|
}
|
||||||
@ -872,7 +1124,6 @@ const getPlatformIcon = (platform) => {
|
|||||||
'naver_blog': 'mdi-web',
|
'naver_blog': 'mdi-web',
|
||||||
'facebook': 'mdi-facebook',
|
'facebook': 'mdi-facebook',
|
||||||
'kakao_story': 'mdi-chat',
|
'kakao_story': 'mdi-chat',
|
||||||
// 백엔드에서 받는 값에 대한 매핑 추가
|
|
||||||
'INSTAGRAM': 'mdi-instagram',
|
'INSTAGRAM': 'mdi-instagram',
|
||||||
'NAVER_BLOG': 'mdi-web',
|
'NAVER_BLOG': 'mdi-web',
|
||||||
'FACEBOOK': 'mdi-facebook',
|
'FACEBOOK': 'mdi-facebook',
|
||||||
@ -887,7 +1138,6 @@ const getPlatformColor = (platform) => {
|
|||||||
'naver_blog': 'green',
|
'naver_blog': 'green',
|
||||||
'facebook': 'blue',
|
'facebook': 'blue',
|
||||||
'kakao_story': 'amber',
|
'kakao_story': 'amber',
|
||||||
// 백엔드에서 받는 값에 대한 매핑 추가
|
|
||||||
'INSTAGRAM': 'pink',
|
'INSTAGRAM': 'pink',
|
||||||
'NAVER_BLOG': 'green',
|
'NAVER_BLOG': 'green',
|
||||||
'FACEBOOK': 'blue',
|
'FACEBOOK': 'blue',
|
||||||
@ -902,7 +1152,6 @@ const getPlatformLabel = (platform) => {
|
|||||||
'naver_blog': '네이버 블로그',
|
'naver_blog': '네이버 블로그',
|
||||||
'facebook': '페이스북',
|
'facebook': '페이스북',
|
||||||
'kakao_story': '카카오스토리',
|
'kakao_story': '카카오스토리',
|
||||||
// 백엔드에서 받는 값에 대한 매핑 추가
|
|
||||||
'INSTAGRAM': '인스타그램',
|
'INSTAGRAM': '인스타그램',
|
||||||
'NAVER_BLOG': '네이버 블로그',
|
'NAVER_BLOG': '네이버 블로그',
|
||||||
'FACEBOOK': '페이스북',
|
'FACEBOOK': '페이스북',
|
||||||
@ -942,50 +1191,40 @@ const formatDateTime = (date) => {
|
|||||||
const truncateText = (text, maxLength) => {
|
const truncateText = (text, maxLength) => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
|
|
||||||
// HTML 태그가 있으면 제거하고 텍스트만 추출
|
|
||||||
const textOnly = text.replace(/<[^>]*>/g, '')
|
const textOnly = text.replace(/<[^>]*>/g, '')
|
||||||
|
|
||||||
if (textOnly.length <= maxLength) return textOnly
|
if (textOnly.length <= maxLength) return textOnly
|
||||||
return textOnly.substring(0, maxLength) + '...'
|
return textOnly.substring(0, maxLength) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML 콘텐츠 감지 함수
|
|
||||||
const isHtmlContent = (content) => {
|
const isHtmlContent = (content) => {
|
||||||
if (!content) return false
|
if (!content) return false
|
||||||
// HTML 태그가 포함되어 있으면 HTML 콘텐츠로 판단
|
|
||||||
return /<[^>]+>/.test(content)
|
return /<[^>]+>/.test(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML에서 텍스트만 추출하는 함수
|
|
||||||
const extractTextFromHtml = (html) => {
|
const extractTextFromHtml = (html) => {
|
||||||
if (!html) return ''
|
if (!html) return ''
|
||||||
// 임시 div 엘리먼트를 만들어서 텍스트만 추출
|
|
||||||
const tempDiv = document.createElement('div')
|
const tempDiv = document.createElement('div')
|
||||||
tempDiv.innerHTML = html
|
tempDiv.innerHTML = html
|
||||||
return tempDiv.textContent || tempDiv.innerText || ''
|
return tempDiv.textContent || tempDiv.innerText || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML 콘텐츠를 축약하는 함수 (미리보기용)
|
|
||||||
const truncateHtmlContent = (html, maxLength) => {
|
const truncateHtmlContent = (html, maxLength) => {
|
||||||
if (!html) return ''
|
if (!html) return ''
|
||||||
|
|
||||||
// 텍스트 길이가 maxLength보다 짧으면 그대로 반환
|
|
||||||
const textContent = extractTextFromHtml(html)
|
const textContent = extractTextFromHtml(html)
|
||||||
if (textContent.length <= maxLength) {
|
if (textContent.length <= maxLength) {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML을 간단하게 축약
|
|
||||||
const tempDiv = document.createElement('div')
|
const tempDiv = document.createElement('div')
|
||||||
tempDiv.innerHTML = html
|
tempDiv.innerHTML = html
|
||||||
|
|
||||||
// 첫 번째 섹션만 가져오기 (보통 제목 부분)
|
|
||||||
const firstSection = tempDiv.querySelector('div[style*="background"]')
|
const firstSection = tempDiv.querySelector('div[style*="background"]')
|
||||||
if (firstSection) {
|
if (firstSection) {
|
||||||
return firstSection.outerHTML
|
return firstSection.outerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
// 첫 번째 섹션이 없으면 텍스트만 축약해서 반환
|
|
||||||
return `<div style="padding: 10px; font-family: 'Noto Sans KR', Arial, sans-serif;">${truncateText(textContent, maxLength)}</div>`
|
return `<div style="padding: 10px; font-family: 'Noto Sans KR', Arial, sans-serif;">${truncateText(textContent, maxLength)}</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1018,11 +1257,6 @@ onMounted(() => {
|
|||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable-header:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 모바일 최적화 */
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.left-panel {
|
.left-panel {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
@ -1039,7 +1273,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HTML 콘텐츠 스타일링 */
|
|
||||||
:deep(.html-content) {
|
:deep(.html-content) {
|
||||||
font-family: 'Noto Sans KR', Arial, sans-serif;
|
font-family: 'Noto Sans KR', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
@ -1055,31 +1288,15 @@ onMounted(() => {
|
|||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.html-content br) {
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.html-content span[style*="color"]) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 해시태그 스타일 보정 */
|
|
||||||
:deep(.html-content span[style*="#1DA1F2"]) {
|
:deep(.html-content span[style*="#1DA1F2"]) {
|
||||||
color: #1976d2 !important; /* Vuetify primary 색상으로 조정 */
|
color: #1976d2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 미리보기 카드 내 HTML 콘텐츠 스타일 */
|
|
||||||
.preview-card :deep(.html-content) {
|
.preview-card :deep(.html-content) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-card :deep(.html-content div[style*="background"]) {
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 미리보기 콘텐츠 최대 높이 제한 */
|
|
||||||
.preview-content {
|
.preview-content {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -1096,14 +1313,4 @@ onMounted(() => {
|
|||||||
background: linear-gradient(transparent, white);
|
background: linear-gradient(transparent, white);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 다이얼로그 내 HTML 콘텐츠 스타일 */
|
|
||||||
.v-dialog :deep(.html-content) {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-dialog :deep(.html-content div[style*="max-width"]) {
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
235
src/views/PosterCreationView.vue
Normal file
235
src/views/PosterCreationView.vue
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
//* src/views/PosterCreationView.vue
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<div class="d-flex align-center mb-6">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-arrow-left"
|
||||||
|
variant="text"
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-h4 font-weight-bold">홍보 포스터 생성</h1>
|
||||||
|
<p class="text-subtitle-1 text-grey-600 mt-1">AI를 활용하여 매력적인 홍보 포스터를 생성하세요</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 메인 컨텐츠 -->
|
||||||
|
<v-row>
|
||||||
|
<!-- 왼쪽: 입력 폼 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="elevation-2">
|
||||||
|
<v-card-title class="bg-primary">
|
||||||
|
<v-icon class="mr-2">mdi-form-textbox</v-icon>
|
||||||
|
포스터 정보 입력
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<PosterForm
|
||||||
|
v-model="posterForm"
|
||||||
|
:loading="loading"
|
||||||
|
@generate="generatePoster"
|
||||||
|
@save="savePoster"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 오른쪽: 미리보기 -->
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="elevation-2">
|
||||||
|
<v-card-title class="bg-secondary">
|
||||||
|
<v-icon class="mr-2">mdi-image-outline</v-icon>
|
||||||
|
미리보기
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<PosterPreview
|
||||||
|
:poster-data="generatedPoster"
|
||||||
|
:loading="loading"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 로딩 오버레이 -->
|
||||||
|
<v-overlay v-model="loading" class="align-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<div class="text-h6 mt-4">{{ loadingMessage }}</div>
|
||||||
|
<div class="text-body-2 text-grey-600 mt-2">{{ loadingSubMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</v-overlay>
|
||||||
|
|
||||||
|
<!-- 성공/오류 스낵바 -->
|
||||||
|
<v-snackbar v-model="showSuccess" color="success" timeout="4000">
|
||||||
|
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||||
|
{{ successMessage }}
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<v-snackbar v-model="showError" color="error" timeout="6000">
|
||||||
|
<v-icon class="mr-2">mdi-alert-circle</v-icon>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import PosterForm from '@/components/poster/PosterForm.vue'
|
||||||
|
import PosterPreview from '@/components/poster/PosterPreview.vue'
|
||||||
|
import { usePosterStore } from '@/store/poster'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 홍보 포스터 생성 페이지
|
||||||
|
* - 포스터 정보 입력 폼
|
||||||
|
* - AI 포스터 생성
|
||||||
|
* - 실시간 미리보기
|
||||||
|
* - 포스터 저장
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 스토어 및 라우터
|
||||||
|
const posterStore = usePosterStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 반응형 데이터
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMessage = ref('')
|
||||||
|
const loadingSubMessage = ref('')
|
||||||
|
|
||||||
|
// 폼 데이터
|
||||||
|
const posterForm = ref({
|
||||||
|
storeId: null,
|
||||||
|
title: '',
|
||||||
|
targetAudience: '메뉴',
|
||||||
|
promotionStartDate: null,
|
||||||
|
promotionEndDate: null,
|
||||||
|
eventName: '',
|
||||||
|
imageStyle: '모던',
|
||||||
|
promotionType: '할인 정보',
|
||||||
|
emotionIntensity: '보통',
|
||||||
|
images: [],
|
||||||
|
category: '이벤트',
|
||||||
|
requirement: '',
|
||||||
|
toneAndManner: '전문적',
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
photoStyle: '밝고 화사한'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 생성된 포스터 데이터
|
||||||
|
const generatedPoster = ref(null)
|
||||||
|
|
||||||
|
// 메시지 상태
|
||||||
|
const showSuccess = ref(false)
|
||||||
|
const showError = ref(false)
|
||||||
|
const successMessage = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 생성
|
||||||
|
*/
|
||||||
|
const generatePoster = async (formData) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
loadingMessage.value = 'AI 포스터 생성 중...'
|
||||||
|
loadingSubMessage.value = '멋진 포스터를 만들고 있어요'
|
||||||
|
|
||||||
|
const result = await posterStore.generatePoster({
|
||||||
|
...formData,
|
||||||
|
storeId: authStore.currentStore?.id || 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
generatedPoster.value = result.data
|
||||||
|
successMessage.value = '포스터가 성공적으로 생성되었습니다!'
|
||||||
|
showSuccess.value = true
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '포스터 생성에 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('포스터 생성 오류:', error)
|
||||||
|
errorMessage.value = error.message || '포스터 생성 중 오류가 발생했습니다.'
|
||||||
|
showError.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포스터 저장
|
||||||
|
*/
|
||||||
|
const savePoster = async () => {
|
||||||
|
if (!generatedPoster.value) {
|
||||||
|
errorMessage.value = '저장할 포스터가 없습니다. 먼저 포스터를 생성해주세요.'
|
||||||
|
showError.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
loadingMessage.value = '포스터 저장 중...'
|
||||||
|
loadingSubMessage.value = '잠시만 기다려주세요'
|
||||||
|
|
||||||
|
const result = await posterStore.savePoster({
|
||||||
|
contentId: generatedPoster.value.contentId,
|
||||||
|
storeId: authStore.currentStore?.id || 1,
|
||||||
|
title: posterForm.value.title,
|
||||||
|
content: generatedPoster.value.content,
|
||||||
|
images: [generatedPoster.value.posterImage],
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
category: posterForm.value.category,
|
||||||
|
requirement: posterForm.value.requirement,
|
||||||
|
toneAndManner: posterForm.value.toneAndManner,
|
||||||
|
emotionIntensity: posterForm.value.emotionIntensity,
|
||||||
|
eventName: posterForm.value.eventName,
|
||||||
|
startDate: posterForm.value.startDate,
|
||||||
|
endDate: posterForm.value.endDate,
|
||||||
|
photoStyle: posterForm.value.photoStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
successMessage.value = '포스터가 성공적으로 저장되었습니다!'
|
||||||
|
showSuccess.value = true
|
||||||
|
|
||||||
|
// 3초 후 콘텐츠 관리 페이지로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/content')
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '포스터 저장에 실패했습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('포스터 저장 오류:', error)
|
||||||
|
errorMessage.value = error.message || '포스터 저장 중 오류가 발생했습니다.'
|
||||||
|
showError.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 초기화
|
||||||
|
onMounted(() => {
|
||||||
|
// 현재 매장 정보 설정
|
||||||
|
if (authStore.currentStore) {
|
||||||
|
posterForm.value.storeId = authStore.currentStore.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card-title {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user