diff --git a/src/services/content.js b/src/services/content.js index b9afd35..17a75ad 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -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} 저장 결과 */ @@ -627,7 +621,21 @@ class ContentService { } /** - * 콘텐츠 상세 조회 + * ✅ 진행 중인 콘텐츠 조회 (첫 번째 코드에서 추가) + * @param {string} period - 조회 기간 + * @returns {Promise} 진행 중인 콘텐츠 목록 + */ + 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} 콘텐츠 상세 정보 */ @@ -641,7 +649,7 @@ class ContentService { } /** - * 콘텐츠 수정 (CON-024: 콘텐츠 수정) + * ✅ 콘텐츠 수정 (CON-024: 콘텐츠 수정) - 두 번째 코드에서 유지 * @param {number} contentId - 콘텐츠 ID * @param {Object} updateData - 수정할 콘텐츠 정보 * @returns {Promise} 수정 결과 @@ -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} 삭제 결과 */ @@ -685,36 +691,24 @@ class ContentService { } /** - * 콘텐츠 상태 변경 (추가 기능) - * @param {number} contentId - 콘텐츠 ID - * @param {string} status - 변경할 상태 - * @returns {Promise} 상태 변경 결과 + * ✅ 타겟 타입을 카테고리로 매핑 (첫 번째 코드에서 추가) + * @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} 복제 결과 - */ - 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} 검색 결과 @@ -741,7 +735,7 @@ class ContentService { } /** - * 콘텐츠 통계 조회 (추가 기능) + * ✅ 콘텐츠 통계 조회 (첫 번째 코드에서 추가) * @param {Object} filters - 필터 조건 * @returns {Promise} 통계 데이터 */ @@ -760,6 +754,78 @@ class ContentService { return handleApiError(error) } } + + /** + * ✅ 콘텐츠 복제 (첫 번째 코드에서 추가) + * @param {number} contentId - 복제할 콘텐츠 ID + * @returns {Promise} 복제 결과 + */ + 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} 상태 변경 결과 + */ + 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} 즐겨찾기 토글 결과 + */ + 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} 템플릿 목록 + */ + 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} 생성 결과 + */ + async generateFromTemplate(templateId, customData = {}) { + try { + const response = await contentApi.post(`/templates/${templateId}/generate`, customData) + return formatSuccessResponse(response.data.data, '템플릿으로 콘텐츠가 생성되었습니다.') + } catch (error) { + return handleApiError(error) + } + } } // 서비스 인스턴스 생성 및 내보내기 diff --git a/src/store/content.js b/src/store/content.js index 7179da3..a9be85a 100644 --- a/src/store/content.js +++ b/src/store/content.js @@ -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) + + // 필터 상태 + const filters = ref({ + contentType: '', + platform: '', + period: '', + sortBy: 'latest' + }) + + // 페이지네이션 + const pagination = ref({ + page: 1, + itemsPerPage: 10 + }) - // 기존 computed 속성들 유지 + // ===== Computed 속성들 ===== const contentCount = computed(() => contentList.value.length) const ongoingContentCount = computed(() => ongoingContents.value.length) + + /** + * 필터링된 콘텐츠 목록 + */ + 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 + }) - // generateContent를 실제 API 호출로 수정 - 단일 파라미터로 변경하고 contentService.generateContent 사용 + /** + * 페이지네이션된 콘텐츠 목록 + */ + 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 } }) \ No newline at end of file diff --git a/src/views/ContentCreationView.vue b/src/views/ContentCreationView.vue index 144ee1a..df5294f 100644 --- a/src/views/ContentCreationView.vue +++ b/src/views/ContentCreationView.vue @@ -388,14 +388,50 @@
-
-
-
- ... 더 보려면 '자세히 보기'를 클릭하세요 + +
+ + + + + + +
+ mdi-image-off + 포스터 이미지가 없습니다
-
{{ truncateText(currentVersion.content, 150) }}
+ + +
+
+
+
+ ... 더 보려면 '자세히 보기'를 클릭하세요 +
+
+
{{ truncateText(currentVersion.content, 150) }}
+
@@ -475,26 +511,33 @@
+ +
mdi-image-off 포스터 이미지가 없습니다 @@ -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 {