release
This commit is contained in:
parent
68f84ab357
commit
4c1381203f
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 서비스 인스턴스 생성 및 내보내기
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user