This commit is contained in:
SeoJHeasdw 2025-06-19 13:53:16 +09:00
parent 68f84ab357
commit 4c1381203f
3 changed files with 661 additions and 113 deletions

View File

@ -1,4 +1,4 @@
//* src/services/content.js - 수정된 완전한 파일
//* src/services/content.js - 최종 완전한 파일 (콘텐츠 관리 기능 통합)
import axios from 'axios'
@ -101,7 +101,7 @@ const handleApiError = (error) => {
/**
* 콘텐츠 서비스 클래스 - 완전 통합 버전
* Java 백엔드 multipart/form-data API와 연동
* Java 백엔드 multipart/form-data API와 연동 + 콘텐츠 관리 기능 통합
*/
class ContentService {
/**
@ -560,10 +560,6 @@ class ContentService {
if (saveData.status) requestData.status = saveData.status
if (saveData.category) requestData.category = saveData.category
if (saveData.requirement) requestData.requirement = saveData.requirement
if (saveData.toneAndManner) requestData.toneAndManner = saveData.toneAndManner
if (saveData.emotionIntensity || saveData.emotionalIntensity) {
requestData.emotionIntensity = saveData.emotionIntensity || saveData.emotionalIntensity
}
if (saveData.eventName) requestData.eventName = saveData.eventName
if (saveData.startDate) requestData.startDate = saveData.startDate
if (saveData.endDate) requestData.endDate = saveData.endDate
@ -594,8 +590,6 @@ class ContentService {
if (saveData.status) requestData.status = saveData.status
if (saveData.category) requestData.category = saveData.category
if (saveData.requirement) requestData.requirement = saveData.requirement
if (saveData.toneAndManner) requestData.toneAndManner = saveData.toneAndManner
if (saveData.emotionIntensity) requestData.emotionIntensity = saveData.emotionIntensity
if (saveData.eventName) requestData.eventName = saveData.eventName
if (saveData.startDate) requestData.startDate = saveData.startDate
if (saveData.endDate) requestData.endDate = saveData.endDate
@ -614,7 +608,7 @@ class ContentService {
}
/**
* 콘텐츠 저장 (통합)
* 콘텐츠 저장 (통합)
* @param {Object} saveData - 저장할 콘텐츠 데이터
* @returns {Promise<Object>} 저장 결과
*/
@ -627,7 +621,21 @@ class ContentService {
}
/**
* 콘텐츠 상세 조회
* 진행 중인 콘텐츠 조회 ( 번째 코드에서 추가)
* @param {string} period - 조회 기간
* @returns {Promise<Object>} 진행 중인 콘텐츠 목록
*/
async getOngoingContents(period = 'month') {
try {
const response = await contentApi.get(`/ongoing?period=${period}`)
return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 콘텐츠 상세 조회 ( 번째 코드에서 유지)
* @param {number} contentId - 콘텐츠 ID
* @returns {Promise<Object>} 콘텐츠 상세 정보
*/
@ -641,7 +649,7 @@ class ContentService {
}
/**
* 콘텐츠 수정 (CON-024: 콘텐츠 수정)
* 콘텐츠 수정 (CON-024: 콘텐츠 수정) - 번째 코드에서 유지
* @param {number} contentId - 콘텐츠 ID
* @param {Object} updateData - 수정할 콘텐츠 정보
* @returns {Promise<Object>} 수정 결과
@ -658,8 +666,6 @@ class ContentService {
if (updateData.status) requestData.status = updateData.status
if (updateData.category) requestData.category = updateData.category
if (updateData.requirement) requestData.requirement = updateData.requirement
if (updateData.toneAndManner) requestData.toneAndManner = updateData.toneAndManner
if (updateData.emotionIntensity) requestData.emotionIntensity = updateData.emotionIntensity
if (updateData.eventName) requestData.eventName = updateData.eventName
if (updateData.images) requestData.images = updateData.images
@ -671,7 +677,7 @@ class ContentService {
}
/**
* 콘텐츠 삭제 (CON-025: 콘텐츠 삭제)
* 콘텐츠 삭제 (CON-025: 콘텐츠 삭제) - 번째 코드에서 유지
* @param {number} contentId - 콘텐츠 ID
* @returns {Promise<Object>} 삭제 결과
*/
@ -685,36 +691,24 @@ class ContentService {
}
/**
* 콘텐츠 상태 변경 (추가 기능)
* @param {number} contentId - 콘텐츠 ID
* @param {string} status - 변경할 상태
* @returns {Promise<Object>} 상태 변경 결과
* 타겟 타입을 카테고리로 매핑 ( 번째 코드에서 추가)
* @param {string} targetType - 타겟 타입
* @returns {string} 매핑된 카테고리
*/
async updateContentStatus(contentId, status) {
try {
const response = await contentApi.patch(`/${contentId}/status`, { status })
return formatSuccessResponse(response.data.data, `콘텐츠 상태가 ${status}로 변경되었습니다.`)
} catch (error) {
return handleApiError(error)
mapTargetToCategory(targetType) {
const mapping = {
'new_menu': '메뉴소개',
'discount': '이벤트',
'store': '인테리어',
'event': '이벤트',
'menu': '메뉴소개',
'service': '서비스'
}
return mapping[targetType] || '이벤트'
}
/**
* 콘텐츠 복제 (추가 기능)
* @param {number} contentId - 복제할 콘텐츠 ID
* @returns {Promise<Object>} 복제 결과
*/
async duplicateContent(contentId) {
try {
const response = await contentApi.post(`/${contentId}/duplicate`)
return formatSuccessResponse(response.data.data, '콘텐츠가 복제되었습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 콘텐츠 검색 (추가 기능)
* 콘텐츠 검색 ( 번째 코드에서 추가)
* @param {string} query - 검색어
* @param {Object} filters - 필터 조건
* @returns {Promise<Object>} 검색 결과
@ -741,7 +735,7 @@ class ContentService {
}
/**
* 콘텐츠 통계 조회 (추가 기능)
* 콘텐츠 통계 조회 (번째 코드에서 추가)
* @param {Object} filters - 필터 조건
* @returns {Promise<Object>} 통계 데이터
*/
@ -760,6 +754,78 @@ class ContentService {
return handleApiError(error)
}
}
/**
* 콘텐츠 복제 ( 번째 코드에서 추가)
* @param {number} contentId - 복제할 콘텐츠 ID
* @returns {Promise<Object>} 복제 결과
*/
async duplicateContent(contentId) {
try {
const response = await contentApi.post(`/${contentId}/duplicate`)
return formatSuccessResponse(response.data.data, '콘텐츠가 복제되었습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 콘텐츠 상태 변경 ( 번째 코드에서 추가)
* @param {number} contentId - 콘텐츠 ID
* @param {string} status - 변경할 상태
* @returns {Promise<Object>} 상태 변경 결과
*/
async updateContentStatus(contentId, status) {
try {
const response = await contentApi.patch(`/${contentId}/status`, { status })
return formatSuccessResponse(response.data.data, `콘텐츠 상태가 ${status}로 변경되었습니다.`)
} catch (error) {
return handleApiError(error)
}
}
/**
* 콘텐츠 즐겨찾기 토글 ( 번째 코드에서 추가)
* @param {number} contentId - 콘텐츠 ID
* @returns {Promise<Object>} 즐겨찾기 토글 결과
*/
async toggleContentFavorite(contentId) {
try {
const response = await contentApi.post(`/${contentId}/favorite`)
return formatSuccessResponse(response.data.data, '즐겨찾기가 변경되었습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 콘텐츠 템플릿 목록 조회 ( 번째 코드에서 추가)
* @param {string} type - 템플릿 타입
* @returns {Promise<Object>} 템플릿 목록
*/
async getContentTemplates(type = 'all') {
try {
const response = await contentApi.get(`/templates?type=${type}`)
return formatSuccessResponse(response.data.data, '콘텐츠 템플릿을 조회했습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 템플릿으로 콘텐츠 생성 ( 번째 코드에서 추가)
* @param {number} templateId - 템플릿 ID
* @param {Object} customData - 커스터마이징 데이터
* @returns {Promise<Object>} 생성 결과
*/
async generateFromTemplate(templateId, customData = {}) {
try {
const response = await contentApi.post(`/templates/${templateId}/generate`, customData)
return formatSuccessResponse(response.data.data, '템플릿으로 콘텐츠가 생성되었습니다.')
} catch (error) {
return handleApiError(error)
}
}
}
// 서비스 인스턴스 생성 및 내보내기

View File

@ -1,22 +1,213 @@
//* src/store/content.js 수정 - generateContent 함수 호출 방식 수정
//* src/store/content.js - 콘텐츠 관리 기능이 통합된 최종 버전
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { ref, computed, readonly } from 'vue'
import contentService from '@/services/content'
import { useAuthStore } from '@/store/auth'
// constants가 없는 경우를 위한 기본값
const PLATFORM_SPECS = {
INSTAGRAM: { name: '인스타그램', maxLength: 2200 },
NAVER_BLOG: { name: '네이버 블로그', maxLength: 10000 },
POSTER: { name: '포스터', maxLength: 500 }
}
const PLATFORM_LABELS = {
INSTAGRAM: '인스타그램',
NAVER_BLOG: '네이버 블로그',
POSTER: '포스터'
}
export const useContentStore = defineStore('content', () => {
// 기존 상태들 유지
// ===== 상태 관리 =====
// 기본 상태
const contentList = ref([])
const contents = ref([]) // ContentManagementView에서 사용하는 속성
const ongoingContents = ref([])
const selectedContent = ref(null)
const generatedContent = ref(null)
const totalCount = ref(0)
// 로딩 상태
const isLoading = ref(false)
const loading = ref(false)
const generating = ref(false)
// 기존 computed 속성들 유지
// 필터 상태
const filters = ref({
contentType: '',
platform: '',
period: '',
sortBy: 'latest'
})
// 페이지네이션
const pagination = ref({
page: 1,
itemsPerPage: 10
})
// ===== Computed 속성들 =====
const contentCount = computed(() => contentList.value.length)
const ongoingContentCount = computed(() => ongoingContents.value.length)
// generateContent를 실제 API 호출로 수정 - 단일 파라미터로 변경하고 contentService.generateContent 사용
/**
* 필터링된 콘텐츠 목록
*/
const filteredContents = computed(() => {
let filtered = [...contentList.value]
if (filters.value.contentType) {
filtered = filtered.filter(content => content.type === filters.value.contentType)
}
if (filters.value.platform) {
filtered = filtered.filter(content => content.platform === filters.value.platform)
}
// 정렬
const sortBy = filters.value.sortBy || 'latest'
if (sortBy.includes('_')) {
const [field, order] = sortBy.split('_')
filtered.sort((a, b) => {
let aValue = a[field]
let bValue = b[field]
if (field === 'createdAt' || field === 'updatedAt') {
aValue = new Date(aValue)
bValue = new Date(bValue)
}
if (order === 'desc') {
return bValue > aValue ? 1 : -1
} else {
return aValue > bValue ? 1 : -1
}
})
} else if (sortBy === 'latest') {
filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
}
return filtered
})
/**
* 페이지네이션된 콘텐츠 목록
*/
const paginatedContents = computed(() => {
const start = (pagination.value.page - 1) * pagination.value.itemsPerPage
const end = start + pagination.value.itemsPerPage
return filteredContents.value.slice(start, end)
})
/**
* 페이지
*/
const totalPages = computed(() => {
return Math.ceil(filteredContents.value.length / pagination.value.itemsPerPage)
})
// ===== 매장 정보 조회 함수 (공통 유틸리티) =====
const getStoreId = async () => {
try {
const userInfo = useAuthStore().user
console.log('사용자 정보:', userInfo)
// 매장 정보 API 호출
const storeApiUrl = (window.__runtime_config__ && window.__runtime_config__.STORE_URL)
? window.__runtime_config__.STORE_URL
: 'http://localhost:8082/api/store'
console.log('매장 API URL:', storeApiUrl)
const token = localStorage.getItem('accessToken') || localStorage.getItem('auth_token') || localStorage.getItem('token')
const storeResponse = await fetch(`${storeApiUrl}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (storeResponse.ok) {
const storeData = await storeResponse.json()
const storeId = storeData.data?.storeId
console.log('✅ 매장 정보 조회 성공, storeId:', storeId)
return storeId
} else {
throw new Error(`매장 정보 조회 실패: ${storeResponse.status}`)
}
} catch (error) {
console.error('❌ 매장 정보 조회 실패:', error)
throw new Error('매장 정보를 조회할 수 없습니다.')
}
}
// ===== 콘텐츠 목록 조회 =====
/**
* 콘텐츠 목록 로딩 (ContentManagementView에서 사용)
*/
const loadContents = async (requestFilters = {}) => {
console.log('=== 콘텐츠 목록 조회 시작 ===')
isLoading.value = true
loading.value = true
try {
// 1단계: 매장 정보 조회하여 실제 storeId 가져오기
const storeId = await getStoreId()
if (!storeId) {
throw new Error('매장 ID를 찾을 수 없습니다.')
}
console.log('조회된 storeId:', storeId)
// 2단계: 조회된 storeId로 콘텐츠 목록 조회
const apiFilters = {
platform: requestFilters.platform || filters.value.platform || null,
storeId: storeId,
sortBy: requestFilters.sortBy || filters.value.sortBy || 'latest'
}
console.log('API 요청 필터:', apiFilters)
const result = await contentService.getContents(apiFilters)
console.log('🔍 contentService.getContents 결과:', result)
console.log('🔍 result.success:', result.success)
console.log('🔍 result.data:', result.data)
console.log('🔍 result.data 타입:', typeof result.data)
console.log('🔍 result.data 길이:', result.data?.length)
if (result.success) {
const responseData = result.data || []
contents.value = responseData
contentList.value = responseData
totalCount.value = responseData.length
console.log('✅ 콘텐츠 로딩 성공:', contents.value.length, '개')
return { success: true }
} else {
console.error('❌ 콘텐츠 로딩 실패:', result.error)
contents.value = []
contentList.value = []
totalCount.value = 0
return { success: false, error: result.error }
}
} catch (error) {
console.error('❌ 콘텐츠 로딩 실패:', error)
contents.value = []
contentList.value = []
totalCount.value = 0
return { success: false, error: error.message || '네트워크 오류가 발생했습니다.' }
} finally {
isLoading.value = false
loading.value = false
}
}
// ===== 기존 API 호출 함수들을 통합된 방식으로 수정 =====
/**
* generateContent를 실제 API 호출로 수정 - 단일 파라미터로 변경하고 contentService.generateContent 사용
*/
const generateContent = async (contentData) => {
generating.value = true
@ -53,7 +244,9 @@ export const useContentStore = defineStore('content', () => {
}
}
// saveContent를 실제 API 호출로 수정 - 단일 파라미터로 변경
/**
* saveContent를 실제 API 호출로 수정 - 단일 파라미터로 변경
*/
const saveContent = async (contentData) => {
isLoading.value = true
@ -84,34 +277,17 @@ export const useContentStore = defineStore('content', () => {
}
}
// fetchContentList를 실제 API 호출로 수정
const fetchContentList = async (filters = {}) => {
isLoading.value = true
try {
const result = await contentService.getContents(filters)
if (result && result.success) {
contentList.value = result.data || []
return { success: true }
} else {
return {
success: false,
error: result?.message || result?.error || '목록 조회에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] fetchContentList 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
/**
* fetchContentList를 실제 API 호출로 수정 (기존 호환성 유지)
*/
const fetchContentList = async (requestFilters = {}) => {
console.log('📋 [STORE] fetchContentList 호출:', requestFilters)
return await loadContents(requestFilters)
}
// fetchOngoingContents를 실제 API 호출로 수정
/**
* fetchOngoingContents를 실제 API 호출로 수정
*/
const fetchOngoingContents = async (period = 'month') => {
isLoading.value = true
@ -138,7 +314,9 @@ export const useContentStore = defineStore('content', () => {
}
}
// 콘텐츠 상세 조회
/**
* 콘텐츠 상세 조회
*/
const fetchContentDetail = async (contentId) => {
isLoading.value = true
@ -165,35 +343,66 @@ export const useContentStore = defineStore('content', () => {
}
}
// 콘텐츠 삭제
// ===== 콘텐츠 관리 기능들 (첫 번째 코드에서 추가) =====
/**
* 콘텐츠 수정
*/
const updateContent = async (contentId, updateData) => {
isLoading.value = true
loading.value = true
try {
const result = await contentService.updateContent(contentId, updateData)
if (result.success) {
await loadContents()
return { success: true, message: '콘텐츠가 수정되었습니다.' }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 콘텐츠 수정 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
isLoading.value = false
loading.value = false
}
}
/**
* 콘텐츠 삭제
*/
const deleteContent = async (contentId) => {
isLoading.value = true
loading.value = true
try {
const result = await contentService.deleteContent(contentId)
if (result && result.success) {
if (result.success) {
// 목록에서 제거
contentList.value = contentList.value.filter(content => content.id !== contentId)
contents.value = contents.value.filter(content => content.id !== contentId)
totalCount.value = contentList.value.length
await loadContents() // 최신 목록으로 새로고침
return { success: true, message: '콘텐츠가 삭제되었습니다.' }
} else {
return {
success: false,
error: result?.message || result?.error || '삭제에 실패했습니다.'
}
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ [STORE] deleteContent 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
console.error('❌ 콘텐츠 삭제 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
isLoading.value = false
loading.value = false
}
}
// 콘텐츠 발행
/**
* 콘텐츠 발행
*/
const publishContent = async (contentId, publishData) => {
isLoading.value = true
@ -221,7 +430,9 @@ export const useContentStore = defineStore('content', () => {
}
}
// 콘텐츠 통계 조회
/**
* 콘텐츠 통계 조회
*/
const fetchContentStats = async (options = {}) => {
isLoading.value = true
@ -247,38 +458,245 @@ export const useContentStore = defineStore('content', () => {
}
}
// 상태 초기화
// ===== 추가된 고급 콘텐츠 관리 기능들 =====
/**
* 콘텐츠 검색
*/
const searchContents = async (query, searchFilters = {}) => {
loading.value = true
try {
const result = await contentService.searchContents(query, searchFilters)
if (result.success) {
contentList.value = result.data || []
contents.value = result.data || []
totalCount.value = result.data?.length || 0
return { success: true }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 콘텐츠 검색 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* 콘텐츠 통계 조회 (추가)
*/
const getContentStats = async (statsFilters = {}) => {
loading.value = true
try {
const result = await contentService.getContentStats(statsFilters)
if (result.success) {
return { success: true, data: result.data }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 콘텐츠 통계 조회 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* 콘텐츠 복제
*/
const duplicateContent = async (contentId) => {
loading.value = true
try {
const result = await contentService.duplicateContent(contentId)
if (result.success) {
await loadContents() // 목록 새로고침
return { success: true, message: '콘텐츠가 복제되었습니다.' }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 콘텐츠 복제 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* 콘텐츠 상태 변경
*/
const updateContentStatus = async (contentId, status) => {
loading.value = true
try {
const result = await contentService.updateContentStatus(contentId, status)
if (result.success) {
await loadContents() // 목록 새로고침
return { success: true, message: `콘텐츠 상태가 ${status}로 변경되었습니다.` }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 콘텐츠 상태 변경 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* 콘텐츠 즐겨찾기 토글
*/
const toggleContentFavorite = async (contentId) => {
loading.value = true
try {
const result = await contentService.toggleContentFavorite(contentId)
if (result.success) {
await loadContents() // 목록 새로고침
return { success: true, message: '즐겨찾기가 변경되었습니다.' }
} else {
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ 즐겨찾기 토글 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
// ===== 유틸리티 메서드들 =====
/**
* 타겟 타입을 카테고리로 매핑
*/
const mapTargetToCategory = (targetType) => {
const mapping = {
'new_menu': '메뉴소개',
'discount': '이벤트',
'store': '인테리어',
'event': '이벤트',
'menu': '메뉴소개',
'service': '서비스'
}
return mapping[targetType] || '메뉴소개'
}
/**
* 플랫폼별 특성 조회
*/
const getPlatformSpec = (platform) => {
return PLATFORM_SPECS?.[platform] || null
}
/**
* 플랫폼 유효성 검사
*/
const validatePlatform = (platform) => {
return PLATFORM_SPECS ? Object.keys(PLATFORM_SPECS).includes(platform) : true
}
/**
* 필터 설정
*/
const setFilters = (newFilters) => {
filters.value = { ...filters.value, ...newFilters }
pagination.value.page = 1 // 필터 변경 시 첫 페이지로
}
/**
* 페이지네이션 설정
*/
const setPagination = (newPagination) => {
pagination.value = { ...pagination.value, ...newPagination }
}
/**
* 상태 초기화
*/
const resetState = () => {
contentList.value = []
contents.value = []
ongoingContents.value = []
selectedContent.value = null
generatedContent.value = null
totalCount.value = 0
filters.value = {
contentType: '',
platform: '',
period: '',
sortBy: 'latest'
}
pagination.value = {
page: 1,
itemsPerPage: 10
}
isLoading.value = false
loading.value = false
generating.value = false
}
return {
// 상태
contentList,
ongoingContents,
selectedContent,
generatedContent,
isLoading,
generating,
// 상태 (readonly로 보호)
contentList: readonly(contentList),
contents: readonly(contents), // ContentManagementView에서 사용
ongoingContents: readonly(ongoingContents),
selectedContent: readonly(selectedContent),
generatedContent: readonly(generatedContent),
totalCount: readonly(totalCount),
isLoading: readonly(isLoading),
loading: readonly(loading),
generating: readonly(generating),
filters: readonly(filters),
pagination: readonly(pagination),
// computed
// 컴퓨티드
contentCount,
ongoingContentCount,
filteredContents,
paginatedContents,
totalPages,
// 액션
// 기본 CRUD 액션들
loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함)
generateContent,
saveContent,
fetchContentList,
fetchContentList, // 기존 호환성 유지
fetchOngoingContents,
fetchContentDetail,
updateContent,
deleteContent,
// 추가 액션들
publishContent,
fetchContentStats,
// 고급 콘텐츠 관리 기능들
searchContents,
getContentStats,
duplicateContent,
updateContentStatus,
toggleContentFavorite,
// 유틸리티
mapTargetToCategory,
getPlatformSpec,
validatePlatform,
setFilters,
setPagination,
resetState
}
})

View File

@ -388,14 +388,50 @@
<!-- 콘텐츠 내용 -->
<div class="text-body-2 mb-3" style="line-height: 1.6;">
<div v-if="isHtmlContent(currentVersion.content)"
class="html-content preview-content">
<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.contentType === 'poster' || currentVersion.type === 'poster'">
<v-img
v-if="currentVersion.posterImage || currentVersion.content"
:src="currentVersion.posterImage || currentVersion.content"
:alt="currentVersion.title"
cover
class="rounded-lg elevation-2 mb-3"
style="max-width: 100%; max-height: 300px; aspect-ratio: 3/4;"
@click="previewImage(currentVersion.posterImage || currentVersion.content, currentVersion.title)"
@error="handleImageError"
>
<template v-slot:placeholder>
<div class="d-flex align-center justify-center fill-height bg-grey-lighten-4">
<v-progress-circular indeterminate color="primary" size="32" />
<span class="ml-2 text-grey">이미지 로딩 ...</span>
</div>
</template>
<template v-slot:error>
<div class="d-flex flex-column align-center justify-center fill-height bg-grey-lighten-3">
<v-icon size="32" color="grey" class="mb-2">mdi-image-broken</v-icon>
<span class="text-caption text-grey">이미지를 불러올 없습니다</span>
</div>
</template>
</v-img>
<div v-else class="d-flex flex-column align-center justify-center bg-grey-lighten-4 rounded-lg pa-8">
<v-icon size="48" color="grey" class="mb-2">mdi-image-off</v-icon>
<span class="text-body-2 text-grey">포스터 이미지가 없습니다</span>
</div>
</div>
<div v-else>{{ truncateText(currentVersion.content, 150) }}</div>
<!-- SNS인 경우 기존 텍스트 표시 -->
<div v-else>
<div v-if="isHtmlContent(currentVersion.content)"
class="html-content preview-content">
<div v-html="truncateHtmlContent(currentVersion.content, 200)"></div>
<div v-if="currentVersion.content.length > 500" class="text-caption text-grey mt-2">
... 보려면 '자세히 보기' 클릭하세요
</div>
</div>
<div v-else>{{ truncateText(currentVersion.content, 150) }}</div>
</div>
</div>
<!-- 해시태그 -->
@ -475,26 +511,33 @@
<!-- 포스터인 경우 이미지로 표시 -->
<div v-if="currentVersion.contentType === 'poster' || currentVersion.type === 'poster'">
<v-img
v-if="getValidImageUrl(currentVersion.posterImage || currentVersion.content)"
:src="getValidImageUrl(currentVersion.posterImage || currentVersion.content)"
v-if="currentVersion.posterImage || currentVersion.content"
:src="currentVersion.posterImage || currentVersion.content"
:alt="currentVersion.title"
cover
class="rounded-lg elevation-2"
style="max-width: 400px; aspect-ratio: 3/4;"
@click="previewImage(getValidImageUrl(currentVersion.posterImage || currentVersion.content), currentVersion.title)"
style="max-width: 400px; aspect-ratio: 3/4; cursor: pointer;"
@click="previewImage(currentVersion.posterImage || currentVersion.content, currentVersion.title)"
@error="handleImageError"
>
<template v-slot:placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-progress-circular indeterminate color="primary" />
<div class="d-flex align-center justify-center fill-height bg-grey-lighten-4">
<v-progress-circular indeterminate color="primary" size="32" />
<span class="ml-2 text-grey">이미지 로딩 ...</span>
</div>
</template>
<template v-slot:error>
<div class="d-flex flex-column align-center justify-center fill-height bg-grey-lighten-3">
<v-icon size="32" color="grey" class="mb-2">mdi-image-broken</v-icon>
<span class="text-caption text-grey">이미지를 불러올 없습니다</span>
<span class="text-caption text-grey mt-1" style="word-break: break-all; max-width: 200px;">
{{ (currentVersion.posterImage || currentVersion.content)?.substring(0, 50) }}...
</span>
</div>
</template>
</v-img>
<div v-else class="d-flex flex-column align-center justify-center bg-grey-lighten-4 rounded-lg pa-8">
<v-icon size="48" color="grey" class="mb-2">mdi-image-off</v-icon>
<span class="text-body-2 text-grey">포스터 이미지가 없습니다</span>
@ -777,18 +820,33 @@ const promotionEndDateRules = [
}
]
// URL
// : URL -
const getValidImageUrl = (imageUrl) => {
if (!imageUrl || typeof imageUrl !== 'string') return null
console.log('🖼️ 이미지 URL 검증:', imageUrl, typeof imageUrl)
// Azure Blob Storage URL, HTTP URL, Data URL
if (!imageUrl || typeof imageUrl !== 'string') {
console.log('❌ 이미지 URL이 문자열이 아님')
return null
}
// -
if (imageUrl.length < 10) {
console.log('❌ 이미지 URL이 너무 짧음:', imageUrl.length)
return null
}
// URL ( )
if (imageUrl.startsWith('http') ||
imageUrl.startsWith('data:image/') ||
imageUrl.startsWith('blob:') ||
imageUrl.startsWith('//')) {
imageUrl.startsWith('//') ||
imageUrl.includes('blob.core.windows.net')) { // Azure Blob Storage
console.log('✅ 유효한 이미지 URL:', imageUrl.substring(0, 50) + '...')
return imageUrl
}
console.log('❌ 유효하지 않은 이미지 URL 형식')
return null
}
@ -800,6 +858,12 @@ const previewImage = (imageUrl, title) => {
window.open(imageUrl, '_blank')
}
// :
const handleImageError = (event) => {
console.error('❌ 이미지 로딩 실패:', event.target?.src)
// (v-img error slot )
}
// : canGenerate computed
const canGenerate = computed(() => {
try {