2025-06-19 16:03:55 +09:00

844 lines
25 KiB
JavaScript

//* src/store/content.js - 콘텐츠 관리 기능이 통합된 최종 버전
import { defineStore } from 'pinia'
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 속성들 =====
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
})
/**
* 페이지네이션된 콘텐츠 목록
*/
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
try {
console.log('🎯 [STORE] generateContent 호출됨:', contentData)
// contentService의 통합 generateContent 함수 사용
const result = await contentService.generateContent(contentData)
console.log('🎯 [STORE] API 응답:', result)
if (result && result.success) {
generatedContent.value = result.data
return {
success: true,
data: result.data,
content: result.data?.content || result.data?.text,
hashtags: result.data?.hashtags || []
}
} else {
return {
success: false,
error: result?.message || result?.error || 'API 응답이 올바르지 않습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] generateContent 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
generating.value = false
}
}
/**
* saveContent를 실제 API 호출로 수정 - 단일 파라미터로 변경
*/
const saveContent = async (contentData) => {
isLoading.value = true
try {
console.log('💾 [STORE] saveContent 호출됨:', contentData)
// contentService의 통합 saveContent 함수 사용
const result = await contentService.saveContent(contentData)
if (result && result.success) {
// 콘텐츠 목록 새로고침
await fetchContentList()
return { success: true, message: '콘텐츠가 저장되었습니다.' }
} else {
return {
success: false,
error: result?.message || result?.error || '저장에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] saveContent 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
}
/**
* 포스터 저장 - 수정된 버전
*/
const savePoster = async (saveData) => {
loading.value = true
try {
console.log('💾 [STORE] 포스터 저장 요청:', saveData)
// 매장 ID 조회 (필요한 경우)
let storeId = saveData.storeId
if (!storeId) {
try {
storeId = await getStoreId()
} catch (error) {
console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error)
storeId = 1
}
}
// ✅ PosterContentSaveRequest 구조에 맞게 데이터 변환 (contentId 처리 개선)
const requestData = {
// ✅ contentId 처리: 값이 있으면 사용, 없으면 임시 ID 생성
contentId: saveData.contentId || Date.now(),
storeId: storeId,
title: saveData.title || '',
// ✅ content 필드에 실제 값 보장 (null이면 안됨)
content: saveData.content || saveData.title || '포스터 콘텐츠',
// ✅ images 배열이 비어있지 않도록 보장
images: Array.isArray(saveData.images) ? saveData.images.filter(img => img) : [],
status: saveData.status || 'PUBLISHED',
category: saveData.category || '이벤트',
requirement: saveData.requirement || '',
toneAndManner: saveData.toneAndManner || '친근함',
emotionIntensity: saveData.emotionIntensity || '보통',
eventName: saveData.eventName || '',
startDate: saveData.startDate,
endDate: saveData.endDate,
photoStyle: saveData.photoStyle || '밝고 화사한'
}
// ✅ 필수 필드 검증
if (!requestData.title) {
throw new Error('제목은 필수입니다.')
}
if (!requestData.images || requestData.images.length === 0) {
throw new Error('이미지는 필수입니다.')
}
console.log('📝 [STORE] 최종 저장 요청 데이터:', {
...requestData,
images: `${requestData.images.length}개 이미지`
})
const result = await contentService.savePoster(requestData)
if (result.success) {
console.log('✅ [STORE] 포스터 저장 성공')
// 목록 새로고침
await loadContents()
return { success: true, message: '포스터가 저장되었습니다.' }
} else {
console.error('❌ [STORE] 포스터 저장 실패:', result.message)
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ [STORE] 포스터 저장 예외:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* SNS 콘텐츠 저장
*/
const saveSnsContent = async (saveData) => {
loading.value = true
try {
console.log('💾 [STORE] SNS 콘텐츠 저장 요청:', saveData)
// 매장 ID 조회 (필요한 경우)
let storeId = saveData.storeId
if (!storeId) {
try {
storeId = await getStoreId()
} catch (error) {
console.warn('⚠️ 매장 ID 조회 실패, 기본값 사용:', error)
storeId = 1
}
}
// SnsContentSaveRequest 구조에 맞게 데이터 변환
const requestData = {
contentId: saveData.contentId || Date.now(), // 임시 ID 생성
storeId: storeId,
platform: saveData.platform || 'INSTAGRAM',
title: saveData.title || '',
content: saveData.content || '',
hashtags: saveData.hashtags || [],
images: saveData.images || [],
finalTitle: saveData.finalTitle || saveData.title,
finalContent: saveData.finalContent || saveData.content,
status: saveData.status || 'DRAFT',
category: saveData.category || '메뉴소개',
requirement: saveData.requirement || '',
toneAndManner: saveData.toneAndManner || '친근함',
emotionIntensity: saveData.emotionIntensity || '보통',
eventName: saveData.eventName || '',
startDate: saveData.startDate,
endDate: saveData.endDate
}
const result = await contentService.saveSnsContent(requestData)
if (result.success) {
console.log('✅ [STORE] SNS 콘텐츠 저장 성공')
// 목록 새로고침
await loadContents()
return { success: true, message: 'SNS 콘텐츠가 저장되었습니다.' }
} else {
console.error('❌ [STORE] SNS 콘텐츠 저장 실패:', result.message)
return { success: false, error: result.message }
}
} catch (error) {
console.error('❌ [STORE] SNS 콘텐츠 저장 예외:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
loading.value = false
}
}
/**
* fetchContentList를 실제 API 호출로 수정 (기존 호환성 유지)
*/
const fetchContentList = async (requestFilters = {}) => {
console.log('📋 [STORE] fetchContentList 호출:', requestFilters)
return await loadContents(requestFilters)
}
/**
* fetchOngoingContents를 실제 API 호출로 수정
*/
const fetchOngoingContents = async (period = 'month') => {
isLoading.value = true
try {
const result = await contentService.getOngoingContents(period)
if (result && result.success) {
ongoingContents.value = result.data || []
return { success: true }
} else {
return {
success: false,
error: result?.message || result?.error || '진행 중인 콘텐츠 조회에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] fetchOngoingContents 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
}
/**
* 콘텐츠 상세 조회
*/
const fetchContentDetail = async (contentId) => {
isLoading.value = true
try {
const result = await contentService.getContentDetail(contentId)
if (result && result.success) {
selectedContent.value = result.data
return { success: true, data: result.data }
} else {
return {
success: false,
error: result?.message || result?.error || '콘텐츠 상세 조회에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] fetchContentDetail 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
}
// ===== 콘텐츠 관리 기능들 =====
/**
* 콘텐츠 수정
*/
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.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 }
}
} catch (error) {
console.error('❌ 콘텐츠 삭제 실패:', error)
return { success: false, error: '네트워크 오류가 발생했습니다.' }
} finally {
isLoading.value = false
loading.value = false
}
}
/**
* 콘텐츠 발행
*/
const publishContent = async (contentId, publishData) => {
isLoading.value = true
try {
const result = await contentService.publishContent(contentId, publishData)
if (result && result.success) {
// 목록 새로고침
await fetchContentList()
return { success: true, message: '콘텐츠가 발행되었습니다.' }
} else {
return {
success: false,
error: result?.message || result?.error || '발행에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] publishContent 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
}
/**
* 콘텐츠 통계 조회
*/
const fetchContentStats = async (options = {}) => {
isLoading.value = true
try {
const result = await contentService.getContentStats(options)
if (result && result.success) {
return { success: true, data: result.data }
} else {
return {
success: false,
error: result?.message || result?.error || '통계 조회에 실패했습니다.'
}
}
} catch (error) {
console.error('❌ [STORE] fetchContentStats 실패:', error)
return {
success: false,
error: error.message || '네트워크 오류가 발생했습니다.'
}
} finally {
isLoading.value = false
}
}
// ===== 추가된 고급 콘텐츠 관리 기능들 =====
/**
* 콘텐츠 검색
*/
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 {
// 상태 (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),
// 컴퓨티드
contentCount,
ongoingContentCount,
filteredContents,
paginatedContents,
totalPages,
// 기본 CRUD 액션들
loadContents, // 새로 추가된 메서드 (매장 정보 조회 포함)
generateContent,
saveContent,
savePoster, // 포스터 전용 저장
saveSnsContent, // SNS 콘텐츠 전용 저장
fetchContentList, // 기존 호환성 유지
fetchOngoingContents,
fetchContentDetail,
updateContent,
deleteContent,
// 추가 액션들
publishContent,
fetchContentStats,
// 고급 콘텐츠 관리 기능들
searchContents,
getContentStats,
duplicateContent,
updateContentStatus,
toggleContentFavorite,
// 유틸리티
mapTargetToCategory,
getPlatformSpec,
validatePlatform,
setFilters,
setPagination,
resetState
}
})