source edit

This commit is contained in:
unknown 2025-06-13 13:03:10 +09:00
parent 336f7dd8eb
commit 66f5af64f4

View File

@ -1,47 +1,56 @@
//* src/views/ContentManagementView.vue
<template>
<v-container fluid class="pa-4">
<!-- 상단 헤더 - 제목과 생성 버튼만 -->
<div class="d-flex justify-space-between align-center mb-6">
<!-- 콘텐츠 생성 버튼 -->
<v-btn
color="primary"
size="large"
prepend-icon="mdi-plus"
@click="$router.push('/content/create')"
>
콘텐츠 생성
</v-btn>
</div>
<!-- 필터 영역 - 통합된 필터 -->
<!-- 필터 영역 -->
<v-card class="mb-6">
<v-card-text>
<v-row align="center">
<!-- 콘텐츠 타입 필터 -->
<v-col cols="12" md="3">
<v-select
v-model="selectedContentType"
:items="contentTypeOptions"
label="콘텐츠 타입"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-filter-variant"
@update:model-value="applyFilters"
/>
<!-- 콘텐츠 타입 필터 - 형태로 변경 -->
<v-col cols="12" md="6">
<div class="d-flex align-center flex-wrap ga-2">
<span class="text-subtitle-2 font-weight-medium mr-2">콘텐츠 타입:</span>
<v-chip
v-for="type in contentTypeOptions"
:key="type.value"
:color="selectedContentType === type.value ? type.color : 'default'"
:variant="selectedContentType === type.value ? 'flat' : 'outlined'"
size="small"
class="mr-1 chip-hover"
@click="selectContentType(type.value)"
>
<span class="mr-1">{{ type.emoji }}</span>
{{ type.title.replace(type.emoji + ' ', '') }}
<span v-if="type.value !== 'all'" class="ml-1">({{ getTypeCount(type.value) }})</span>
<span v-else class="ml-1">({{ getTotalCount() }})</span>
</v-chip>
</div>
</v-col>
<!-- 제목 검색 -->
<v-col cols="12" md="4">
<v-text-field
v-model="searchQuery"
label="제목, 해시태그로 검색"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
clearable
@update:model-value="applyFilters"
/>
<!-- 검색 새콘텐츠생성 버튼 -->
<v-col cols="12" md="6">
<div class="d-flex align-center ga-2">
<!-- 제목 검색 -->
<v-text-field
v-model="searchQuery"
label="제목, 해시태그로 검색"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
clearable
hide-details
@update:model-value="applyFilters"
/>
<!-- 콘텐츠 생성 버튼 -->
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="$router.push('/content/create')"
class="ml-2"
>
새콘텐츠생성
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
@ -143,21 +152,7 @@
</v-chip>
</div>
<div class="text-caption text-truncate grey--text" style="max-width: 400px;">
{{ content.content }}
</div>
<div v-if="content.hashtags?.length" class="mt-1">
<v-chip
v-for="tag in content.hashtags.slice(0, 3)"
:key="tag"
size="x-small"
variant="outlined"
class="mr-1"
>
#{{ tag }}
</v-chip>
<span v-if="content.hashtags.length > 3" class="text-caption text-grey-600">
+{{ content.hashtags.length - 3 }}
</span>
{{ content.content ? content.content.substring(0, 80) + '...' : '' }}
</div>
</div>
</td>
@ -167,9 +162,6 @@
size="small"
variant="tonal"
>
<v-icon start size="16" :color="getPlatformColor(content.platform)">
{{ getPlatformIcon(content.platform) }}
</v-icon>
{{ getPlatformText(content.platform) }}
</v-chip>
</td>
@ -179,13 +171,18 @@
</div>
</td>
<td @click.stop>
<div class="d-flex ga-2">
<div class="d-flex ga-1">
<v-btn
icon="mdi-eye"
size="small"
variant="text"
@click="showDetail(content)"
/>
<v-btn
icon="mdi-pencil"
size="small"
variant="text"
color="primary"
@click="editContent(content)"
@click="showDetailWithEdit(content)"
/>
<v-btn
icon="mdi-delete"
@ -201,62 +198,64 @@
</v-table>
</div>
</v-card-text>
<!-- 페이지네이션 -->
<v-card-actions v-if="totalPages > 1" class="justify-center">
<v-pagination
v-model="currentPage"
:length="totalPages"
:total-visible="7"
circle
/>
</v-card-actions>
</v-card>
<!-- 상세보기/편집 다이얼로그 -->
<v-dialog v-model="showDetailDialog" max-width="800" scrollable>
<v-card v-if="selectedContent">
<!-- 페이지네이션 -->
<div v-if="totalPages > 1" class="d-flex justify-center mt-6">
<v-pagination
v-model="currentPage"
:length="totalPages"
:total-visible="7"
color="primary"
@update:model-value="scrollToTop"
/>
</div>
<!-- 상세 보기/수정 다이얼로그 -->
<v-dialog v-model="showDetailDialog" max-width="800px" scrollable>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span>{{ isEditMode ? '콘텐츠 편집' : '콘텐츠 상세보기' }}</span>
<v-btn icon="mdi-close" variant="text" @click="showDetailDialog = false" />
<span>{{ isEditMode ? '콘텐츠 수정' : '콘텐츠 상세 정보' }}</span>
<v-btn
icon="mdi-close"
variant="text"
@click="closeDialog"
/>
</v-card-title>
<v-divider />
<v-card-text style="max-height: 600px;">
<v-form ref="editForm" v-model="editFormValid">
<v-card-text class="pa-4" style="max-height: 70vh;">
<v-form ref="editForm" v-model="editFormValid" v-if="selectedContent">
<!-- 제목 -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">제목</label>
<v-text-field
v-if="isEditMode"
v-model="editingContent.title"
:rules="titleRules"
variant="outlined"
density="compact"
:rules="titleRules"
/>
<div v-else class="text-body-1">{{ selectedContent.title }}</div>
<div v-else class="text-body-1 font-weight-medium">
{{ selectedContent.title }}
</div>
</div>
<!-- 플랫폼 (수정 비활성화) -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">플랫폼</label>
<div class="d-flex align-center">
<v-chip :color="getPlatformColor(selectedContent.platform)" variant="tonal">
<v-icon start size="16" :color="getPlatformColor(selectedContent.platform)">
{{ getPlatformIcon(selectedContent.platform) }}
</v-icon>
{{ getPlatformText(selectedContent.platform) }}
</v-chip>
<span v-if="isEditMode" class="text-caption text-grey-600 ml-2">
(플랫폼은 수정할 없습니다)
</span>
<v-chip :color="getPlatformColor(selectedContent.platform)" variant="tonal">
{{ getPlatformText(selectedContent.platform) }}
</v-chip>
<div v-if="isEditMode" class="text-caption text-grey-600 mt-1">
플랫폼은 수정할 없습니다.
</div>
</div>
<!-- 프로모션 기간 -->
<!-- 홍보 기간 -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">프로모션 기간</label>
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">홍보 기간</label>
<div v-if="isEditMode" class="d-flex ga-2">
<v-text-field
v-model="editingContent.startDate"
@ -278,15 +277,12 @@
</div>
</div>
<!-- 콘텐츠 내용 (수정 비활성화) -->
<!-- 내용 (수정 비활성화) -->
<div class="mb-4">
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">콘텐츠 내용</label>
<div v-if="isEditMode">
<div class="pa-3 bg-grey-lighten-4 rounded text-body-2" style="white-space: pre-wrap;">
{{ selectedContent.content }}
</div>
<div class="text-caption text-grey-600 mt-1">
콘텐츠 내용은 수정할 없습니다. 새로 생성해주세요.
<label class="text-subtitle-2 font-weight-medium mb-2 d-block">내용</label>
<div v-if="isEditMode" class="pa-3 bg-grey-lighten-4 rounded">
<div class="text-body-2 text-grey-600 mb-2">
내용은 수정할 없습니다. 새로 생성해주세요.
</div>
</div>
<div v-else class="text-body-1" style="white-space: pre-wrap;">
@ -444,10 +440,10 @@ const errorMessage = ref('')
//
const contentTypeOptions = [
{ title: '전체', value: 'all' },
{ title: 'Instagram', value: 'instagram' },
{ title: '네이버 블로그', value: 'blog' },
{ title: '포스터', value: 'poster' }
{ title: '📊 전체', value: 'all', color: 'primary', emoji: '📊' },
{ title: '📷 Instagram', value: 'instagram', color: 'pink', emoji: '📷' },
{ title: '📝 네이버 블로그', value: 'blog', color: 'green', emoji: '📝' },
{ title: '🎨 포스터', value: 'poster', color: 'orange', emoji: '🎨' }
]
const statusOptions = [
@ -474,7 +470,17 @@ const filteredContents = computed(() => {
//
if (selectedContentType.value !== 'all') {
contents = contents.filter(content => content.platform === selectedContentType.value)
contents = contents.filter(content => {
//
const platformMapping = {
'instagram': ['instagram', 'INSTAGRAM'],
'blog': ['blog', 'NAVER_BLOG', 'naver_blog'],
'poster': ['poster', 'POSTER']
}
const allowedPlatforms = platformMapping[selectedContentType.value] || [selectedContentType.value]
return allowedPlatforms.includes(content.platform)
})
}
//
@ -539,6 +545,135 @@ const totalPages = computed(() => {
return Math.ceil(filteredContents.value.length / itemsPerPage.value)
})
//
const loadContents = async () => {
loading.value = true
try {
await contentStore.loadContents()
} catch (error) {
console.error('콘텐츠 로딩 실패:', error)
showError.value = true
errorMessage.value = '콘텐츠를 불러오는데 실패했습니다.'
} finally {
loading.value = false
}
}
const selectContentType = (type) => {
selectedContentType.value = type
currentPage.value = 1
// ( )
}
const applyFilters = () => {
currentPage.value = 1
}
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const showDetail = (content) => {
selectedContent.value = { ...content }
isEditMode.value = false
showDetailDialog.value = true
}
const showDetailWithEdit = (content) => {
selectedContent.value = { ...content }
editingContent.value = { ...content }
isEditMode.value = true
showDetailDialog.value = true
}
const showEditMode = () => {
editingContent.value = { ...selectedContent.value }
isEditMode.value = true
}
const cancelEdit = () => {
isEditMode.value = false
editingContent.value = null
}
const saveEdit = async () => {
if (!editForm.value.validate()) return
updating.value = true
try {
await contentStore.updateContent(editingContent.value.id, editingContent.value)
selectedContent.value = { ...editingContent.value }
isEditMode.value = false
editingContent.value = null
showSuccess.value = true
successMessage.value = '콘텐츠가 성공적으로 수정되었습니다.'
} catch (error) {
console.error('콘텐츠 수정 실패:', error)
showError.value = true
errorMessage.value = '콘텐츠 수정에 실패했습니다.'
} finally {
updating.value = false
}
}
const closeDialog = () => {
showDetailDialog.value = false
isEditMode.value = false
selectedContent.value = null
editingContent.value = null
}
const confirmDelete = async (content) => {
if (confirm(`"${content.title}" 콘텐츠를 삭제하시겠습니까?`)) {
try {
await contentStore.deleteContent(content.id)
showSuccess.value = true
successMessage.value = '콘텐츠가 성공적으로 삭제되었습니다.'
closeDialog()
} catch (error) {
console.error('콘텐츠 삭제 실패:', error)
showError.value = true
errorMessage.value = '콘텐츠 삭제에 실패했습니다.'
}
}
}
const deleteSelectedItems = async () => {
if (selectedItems.value.length === 0) return
if (confirm(`선택된 ${selectedItems.value.length}개의 콘텐츠를 삭제하시겠습니까?`)) {
try {
await Promise.all(selectedItems.value.map(id => contentStore.deleteContent(id)))
selectedItems.value = []
selectAll.value = false
showSuccess.value = true
successMessage.value = '선택된 콘텐츠가 성공적으로 삭제되었습니다.'
} catch (error) {
console.error('콘텐츠 일괄 삭제 실패:', error)
showError.value = true
errorMessage.value = '콘텐츠 삭제에 실패했습니다.'
}
}
}
const toggleSelectAll = () => {
if (selectAll.value) {
selectedItems.value = paginatedContents.value.map(content => content.id)
} else {
selectedItems.value = []
}
}
const sortByPromotionDate = () => {
if (promotionSortOrder.value === 'none') {
promotionSortOrder.value = 'asc'
} else if (promotionSortOrder.value === 'asc') {
promotionSortOrder.value = 'desc'
} else {
promotionSortOrder.value = 'none'
}
}
//
const getTotalCount = () => {
return contentStore.contents?.length || 0
@ -546,7 +681,17 @@ const getTotalCount = () => {
const getTypeCount = (type) => {
if (type === 'all') return getTotalCount()
return contentStore.contents?.filter(content => content.platform === type).length || 0
return contentStore.contents?.filter(content => {
//
const platformMapping = {
'instagram': ['instagram', 'INSTAGRAM'],
'blog': ['blog', 'NAVER_BLOG', 'naver_blog'],
'poster': ['poster', 'POSTER']
}
const allowedPlatforms = platformMapping[type] || [type]
return allowedPlatforms.includes(content.platform)
}).length || 0
}
const getPlatformColor = (platform) => {
@ -561,18 +706,6 @@ const getPlatformColor = (platform) => {
return colors[platform] || 'grey'
}
const getPlatformIcon = (platform) => {
const icons = {
'instagram': 'mdi-instagram',
'INSTAGRAM': 'mdi-instagram',
'blog': 'mdi-blogger',
'NAVER_BLOG': 'mdi-blogger',
'poster': 'mdi-file-image',
'POSTER': 'mdi-file-image'
}
return icons[platform] || 'mdi-web'
}
const getPlatformText = (platform) => {
const texts = {
'instagram': 'Instagram',
@ -588,9 +721,7 @@ const getPlatformText = (platform) => {
const getStatusColor = (status) => {
const colors = {
'published': 'success',
'PUBLISHED': 'success',
'draft': 'warning',
'DRAFT': 'warning',
'archived': 'grey'
}
return colors[status] || 'grey'
@ -599,173 +730,36 @@ const getStatusColor = (status) => {
const getStatusText = (status) => {
const texts = {
'published': '발행됨',
'PUBLISHED': '발행됨',
'draft': '임시저장',
'DRAFT': '임시저장',
'archived': '보관됨'
}
return texts[status] || status
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\./g, '.').replace(/ /g, '')
}
const formatDateRange = (startDate, endDate) => {
if (!startDate || !endDate) return '-'
if (!startDate && !endDate) return '-'
const start = new Date(startDate).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\./g, '.').replace(/ /g, '')
const end = new Date(endDate).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\./g, '.').replace(/ /g, '')
return `${start} ~ ${end}`
}
//
const sortByPromotionDate = () => {
//
if (promotionSortOrder.value === 'none') {
promotionSortOrder.value = 'asc'
} else if (promotionSortOrder.value === 'asc') {
promotionSortOrder.value = 'desc'
} else {
promotionSortOrder.value = 'none'
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
//
if (promotionSortOrder.value !== 'none') {
sortBy.value = 'latest' //
const start = formatDate(startDate)
const end = formatDate(endDate)
if (start && end) {
return `${start} ~ ${end}`
} else if (start) {
return `${start} ~`
} else if (end) {
return `~ ${end}`
}
currentPage.value = 1
}
//
const applyFilters = () => {
//
promotionSortOrder.value = 'none'
currentPage.value = 1
}
const applySorting = () => {
//
promotionSortOrder.value = 'none'
currentPage.value = 1
}
//
const toggleSelectAll = () => {
if (selectAll.value) {
selectedItems.value = paginatedContents.value.map(content => content.id)
} else {
selectedItems.value = []
}
}
const deleteSelectedItems = async () => {
if (selectedItems.value.length === 0) return
if (confirm(`선택된 ${selectedItems.value.length}개의 콘텐츠를 삭제하시겠습니까?`)) {
try {
await contentStore.deleteMultipleContents(selectedItems.value)
selectedItems.value = []
selectAll.value = false
showSuccessMessage('선택된 콘텐츠가 삭제되었습니다.')
} catch (error) {
showErrorMessage('콘텐츠 삭제 중 오류가 발생했습니다.')
}
}
}
//
const showDetail = (content) => {
selectedContent.value = content
showDetailDialog.value = true
isEditMode.value = false
}
const showEditMode = () => {
editingContent.value = {
...selectedContent.value,
hashtags: [...(selectedContent.value.hashtags || [])]
}
isEditMode.value = true
}
//
const editContent = (content) => {
selectedContent.value = content
editingContent.value = {
...content,
hashtags: [...(content.hashtags || [])]
}
isEditMode.value = true
showDetailDialog.value = true
}
const cancelEdit = () => {
editingContent.value = null
isEditMode.value = false
}
const saveEdit = async () => {
if (!editFormValid.value) return
updating.value = true
try {
await contentStore.updateContent(editingContent.value.id, editingContent.value)
selectedContent.value = { ...editingContent.value }
showSuccessMessage('콘텐츠가 수정되었습니다.')
isEditMode.value = false
editingContent.value = null
} catch (error) {
showErrorMessage('콘텐츠 수정 중 오류가 발생했습니다.')
} finally {
updating.value = false
}
}
//
const confirmDelete = (content) => {
if (confirm(`"${content.title}" 콘텐츠를 삭제하시겠습니까?`)) {
deleteContent(content.id)
}
}
const deleteContent = async (contentId) => {
try {
await contentStore.deleteContent(contentId)
showSuccessMessage('콘텐츠가 삭제되었습니다.')
if (showDetailDialog.value) {
showDetailDialog.value = false
}
} catch (error) {
showErrorMessage('콘텐츠 삭제 중 오류가 발생했습니다.')
}
}
//
const showSuccessMessage = (message) => {
successMessage.value = message
showSuccess.value = true
}
const showErrorMessage = (message) => {
errorMessage.value = message
showError.value = true
return '-'
}
//
@ -774,7 +768,9 @@ onMounted(async () => {
try {
await contentStore.loadContents()
} catch (error) {
showErrorMessage('콘텐츠를 불러오는 중 오류가 발생했습니다.')
console.error('콘텐츠 로딩 실패:', error)
showError.value = true
errorMessage.value = '콘텐츠를 불러오는데 실패했습니다.'
} finally {
loading.value = false
}
@ -818,6 +814,28 @@ watch(promotionSortOrder, (newVal) => {
transition: color 0.2s ease-in-out;
}
/* 칩 hover 효과 강화 */
.v-chip {
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.v-chip:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chip-hover:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* 선택된 칩 강조 */
.v-chip.v-chip--variant-flat {
font-weight: 600;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
/* 버튼 hover 효과 */
.v-btn {
transition: all 0.2s ease-in-out;