storeManagement contentmanagement edit
This commit is contained in:
parent
f8c7ecc261
commit
02765ba943
@ -1,6 +1,23 @@
|
|||||||
//* src/views/ContentManagementView.vue
|
//* src/views/ContentManagementView.vue
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex align-center mb-4">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="mr-3"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<h1 class="text-h5">📝 콘텐츠 관리</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-subtitle-1 grey--text">생성된 콘텐츠를 관리하고 성과를 분석합니다</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- 콘텐츠 타입 필터 (상단 이동) -->
|
<!-- 콘텐츠 타입 필터 (상단 이동) -->
|
||||||
<v-row class="mb-4">
|
<v-row class="mb-4">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
@ -10,8 +27,8 @@
|
|||||||
<div class="d-flex align-center flex-wrap">
|
<div class="d-flex align-center flex-wrap">
|
||||||
<div class="text-subtitle-2 mr-4 mb-2">콘텐츠 타입:</div>
|
<div class="text-subtitle-2 mr-4 mb-2">콘텐츠 타입:</div>
|
||||||
<v-chip-group
|
<v-chip-group
|
||||||
v-model="selectedContentTypes"
|
v-model="selectedContentType"
|
||||||
multiple
|
mandatory
|
||||||
@update:model-value="applyContentTypeFilter"
|
@update:model-value="applyContentTypeFilter"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
>
|
>
|
||||||
@ -75,8 +92,76 @@
|
|||||||
|
|
||||||
<!-- 메인 콘텐츠 영역 -->
|
<!-- 메인 콘텐츠 영역 -->
|
||||||
<v-row>
|
<v-row>
|
||||||
<!-- 콘텐츠 목록 -->
|
<!-- 좌측 사이드바 필터 - Desktop 비율 수정 -->
|
||||||
<v-col cols="12" md="9">
|
<v-col cols="12" lg="3" md="4">
|
||||||
|
<v-card elevation="2" height="400" class="sticky-sidebar">
|
||||||
|
<v-card-title class="text-h6 pa-4">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-filter</v-icon>
|
||||||
|
추가 필터
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<!-- 상태 필터 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-subtitle-2 mb-2">상태</div>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="filters.published"
|
||||||
|
label="게시됨 (18)"
|
||||||
|
color="success"
|
||||||
|
@change="applyFilters"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
<v-checkbox
|
||||||
|
v-model="filters.draft"
|
||||||
|
label="임시저장 (6)"
|
||||||
|
color="orange"
|
||||||
|
@change="applyFilters"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기간 필터 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-subtitle-2 mb-2">기간</div>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.period"
|
||||||
|
label="전체 기간"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:items="periodOptions"
|
||||||
|
@update:model-value="applyFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 정렬 필터 -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-subtitle-2 mb-2">정렬</div>
|
||||||
|
<v-select
|
||||||
|
v-model="sortBy"
|
||||||
|
label="최신순"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:items="sortOptions"
|
||||||
|
@update:model-value="applySorting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 필터 초기화 -->
|
||||||
|
<v-btn
|
||||||
|
color="grey"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
@click="resetFilters"
|
||||||
|
>
|
||||||
|
<v-icon class="mr-1">mdi-refresh</v-icon>
|
||||||
|
필터 초기화
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- 콘텐츠 목록 - Desktop 비율 수정 -->
|
||||||
|
<v-col cols="12" lg="9" md="8">
|
||||||
<v-card elevation="2">
|
<v-card elevation="2">
|
||||||
<!-- 상단 툴바 -->
|
<!-- 상단 툴바 -->
|
||||||
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
@ -102,6 +187,29 @@
|
|||||||
선택 삭제 ({{ selectedItems.length }})
|
선택 삭제 ({{ selectedItems.length }})
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 뷰 옵션 -->
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="viewMode"
|
||||||
|
mandatory
|
||||||
|
class="mr-3"
|
||||||
|
>
|
||||||
|
<v-btn icon size="small" value="list">
|
||||||
|
<v-icon>mdi-view-list</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon size="small" value="grid">
|
||||||
|
<v-icon>mdi-view-grid</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
|
||||||
|
<!-- 새 콘텐츠 생성 -->
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="$router.push('/content/create')"
|
||||||
|
>
|
||||||
|
<v-icon class="mr-1">mdi-plus</v-icon>
|
||||||
|
새 콘텐츠
|
||||||
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
@ -131,63 +239,113 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 리스트 뷰 -->
|
<!-- 리스트 뷰 - 테이블 형태 -->
|
||||||
<div v-else-if="viewMode === 'list'">
|
<div v-else-if="viewMode === 'list'">
|
||||||
<v-list>
|
<v-table>
|
||||||
<template v-for="(content, index) in paginatedContents" :key="content.id">
|
<thead>
|
||||||
<v-list-item
|
<tr>
|
||||||
class="px-4 py-3"
|
<th width="50">
|
||||||
@click="showDetail(content)"
|
<v-checkbox
|
||||||
>
|
v-model="selectAll"
|
||||||
<template #prepend>
|
@change="toggleSelectAll"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th width="450">제목</th>
|
||||||
|
<th width="150">플랫폼</th>
|
||||||
|
<th width="150">생성일</th>
|
||||||
|
<th width="120">액션</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="content in paginatedContents" :key="content.id" class="cursor-pointer" @click="showDetail(content)">
|
||||||
|
<td @click.stop>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="selectedItems"
|
v-model="selectedItems"
|
||||||
:value="content.id"
|
:value="content.id"
|
||||||
@click.stop
|
|
||||||
density="compact"
|
density="compact"
|
||||||
/>
|
/>
|
||||||
</template>
|
</td>
|
||||||
|
<td>
|
||||||
<v-list-item-title class="d-flex align-center">
|
<div class="d-flex flex-column">
|
||||||
|
<div class="d-flex align-center mb-1">
|
||||||
|
<span class="font-weight-medium text-subtitle-2 mr-2">{{ content.title }}</span>
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(content.status)"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ getStatusText(content.status) }}
|
||||||
|
</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"
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</v-chip>
|
||||||
|
<span v-if="content.hashtags.length > 3" class="text-caption grey--text">
|
||||||
|
+{{ content.hashtags.length - 3 }}개
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<v-chip
|
<v-chip
|
||||||
:color="getPlatformColor(content.platform)"
|
:color="getPlatformColor(content.platform)"
|
||||||
size="small"
|
size="small"
|
||||||
class="mr-3"
|
variant="tonal"
|
||||||
>
|
>
|
||||||
<v-icon class="mr-1" size="14">{{ getPlatformIcon(content.platform) }}</v-icon>
|
<v-icon class="mr-1" size="14">{{ getPlatformIcon(content.platform) }}</v-icon>
|
||||||
{{ getPlatformText(content.platform) }}
|
{{ getPlatformText(content.platform) }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<span class="font-weight-medium">{{ content.title }}</span>
|
</td>
|
||||||
</v-list-item-title>
|
<td>
|
||||||
|
<div class="text-body-2">{{ formatDate(content.createdAt) }}</div>
|
||||||
<v-list-item-subtitle class="mt-1">
|
<div class="text-caption grey--text">{{ formatTime(content.createdAt) }}</div>
|
||||||
<div class="d-flex align-center justify-space-between">
|
</td>
|
||||||
<div class="d-flex align-center">
|
<td @click.stop>
|
||||||
<v-chip
|
<div class="d-flex">
|
||||||
:color="getStatusColor(content.status)"
|
|
||||||
size="x-small"
|
|
||||||
class="mr-2"
|
|
||||||
>
|
|
||||||
{{ getStatusText(content.status) }}
|
|
||||||
</v-chip>
|
|
||||||
<span class="text-caption">{{ formatDateTime(content.createdAt) }}</span>
|
|
||||||
<span class="text-caption ml-3">조회수: {{ formatNumber(content.views || 0) }}</span>
|
|
||||||
</div>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click.stop="confirmDelete(content)"
|
@click="showDetail(content)"
|
||||||
|
class="mr-1"
|
||||||
>
|
>
|
||||||
<v-icon size="16">mdi-delete-outline</v-icon>
|
<v-icon size="16">mdi-eye</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editContent(content)"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-pencil</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="confirmDelete(content)"
|
||||||
|
>
|
||||||
|
<v-icon size="16">mdi-delete</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-list-item-subtitle>
|
</td>
|
||||||
</v-list-item>
|
</tr>
|
||||||
|
</tbody>
|
||||||
<v-divider v-if="index < paginatedContents.length - 1" />
|
</v-table>
|
||||||
</template>
|
|
||||||
</v-list>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 그리드 뷰 -->
|
<!-- 그리드 뷰 -->
|
||||||
@ -199,6 +357,7 @@
|
|||||||
cols="12"
|
cols="12"
|
||||||
sm="6"
|
sm="6"
|
||||||
md="4"
|
md="4"
|
||||||
|
lg="3"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
elevation="2"
|
elevation="2"
|
||||||
@ -410,7 +569,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useContentStore } from '@/store/content'
|
import { useContentStore } from '@/store/content'
|
||||||
|
|
||||||
@ -434,8 +593,8 @@ const selectedItems = ref([])
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const itemsPerPage = ref(20)
|
const itemsPerPage = ref(20)
|
||||||
|
|
||||||
// 콘텐츠 타입 필터 (새로운 방식)
|
// 콘텐츠 타입 필터 (단순한 방식으로 변경)
|
||||||
const selectedContentTypes = ref(['all'])
|
const selectedContentType = ref('all')
|
||||||
|
|
||||||
// 기존 필터 상태
|
// 기존 필터 상태
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
@ -478,6 +637,9 @@ const sortOptions = [
|
|||||||
{ title: '조회수순', value: 'views' }
|
{ title: '조회수순', value: 'views' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 콘텐츠 타입 옵션 제거 (더 이상 필요 없음)
|
||||||
|
// const contentTypeOptions = [...]
|
||||||
|
|
||||||
// 계산된 속성
|
// 계산된 속성
|
||||||
const filteredContents = computed(() => {
|
const filteredContents = computed(() => {
|
||||||
let contents = contentStore.contents || []
|
let contents = contentStore.contents || []
|
||||||
@ -494,10 +656,10 @@ const filteredContents = computed(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 콘텐츠 타입 필터링
|
// 콘텐츠 타입 필터링 (수정된 로직)
|
||||||
if (!selectedContentTypes.value.includes('all')) {
|
if (selectedContentType.value && selectedContentType.value !== 'all') {
|
||||||
contents = contents.filter(content =>
|
contents = contents.filter(content =>
|
||||||
selectedContentTypes.value.includes(content.platform)
|
content.platform === selectedContentType.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,21 +737,8 @@ const getTypeCount = (type) => {
|
|||||||
return contentStore.contents?.filter(content => content.platform === type).length || 0
|
return contentStore.contents?.filter(content => content.platform === type).length || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터 메서드
|
// 필터 메서드 (수정된 로직)
|
||||||
const applyContentTypeFilter = () => {
|
const applyContentTypeFilter = () => {
|
||||||
// 전체 선택시 다른 필터 해제
|
|
||||||
if (selectedContentTypes.value.includes('all')) {
|
|
||||||
selectedContentTypes.value = ['all']
|
|
||||||
} else {
|
|
||||||
// 다른 필터 선택시 전체 해제
|
|
||||||
selectedContentTypes.value = selectedContentTypes.value.filter(type => type !== 'all')
|
|
||||||
|
|
||||||
// 아무것도 선택되지 않으면 전체 선택
|
|
||||||
if (selectedContentTypes.value.length === 0) {
|
|
||||||
selectedContentTypes.value = ['all']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -602,7 +751,7 @@ const applySorting = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
selectedContentTypes.value = ['all']
|
selectedContentType.value = 'all' // 전체 선택
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
filters.value = {
|
filters.value = {
|
||||||
published: false,
|
published: false,
|
||||||
@ -649,6 +798,14 @@ const showEditMode = () => {
|
|||||||
isEditMode.value = true
|
isEditMode.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 편집 관련 메서드
|
||||||
|
const editContent = (content) => {
|
||||||
|
selectedContent.value = content
|
||||||
|
editingContent.value = { ...content }
|
||||||
|
isEditMode.value = true
|
||||||
|
showDetailDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const cancelEdit = () => {
|
const cancelEdit = () => {
|
||||||
editingContent.value = null
|
editingContent.value = null
|
||||||
isEditMode.value = false
|
isEditMode.value = false
|
||||||
@ -726,6 +883,25 @@ const getStatusColor = (status) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateTime) => {
|
||||||
|
if (!dateTime) return '-'
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
return date.toLocaleDateString('ko-KR', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (dateTime) => {
|
||||||
|
if (!dateTime) return '-'
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
return date.toLocaleTimeString('ko-KR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const formatDateTime = (dateTime) => {
|
const formatDateTime = (dateTime) => {
|
||||||
if (!dateTime) return '-'
|
if (!dateTime) return '-'
|
||||||
const date = new Date(dateTime)
|
const date = new Date(dateTime)
|
||||||
@ -758,6 +934,11 @@ const showErrorMessage = (message) => {
|
|||||||
showError.value = true
|
showError.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectedItems 변화 감지
|
||||||
|
watch(selectedItems, (newVal) => {
|
||||||
|
selectAll.value = newVal.length === paginatedContents.value.length && paginatedContents.value.length > 0
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// 라이프사이클
|
// 라이프사이클
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -772,6 +953,48 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 테이블 스타일 - 개선된 Border */
|
||||||
|
.v-table {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table thead th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table thead th:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table tbody td {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table tbody td:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table tbody tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.v-chip-group {
|
.v-chip-group {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@ -790,6 +1013,36 @@ onMounted(async () => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop 레이아웃 최적화 */
|
||||||
|
@media (min-width: 1264px) {
|
||||||
|
.v-col-lg-3 {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-col-lg-9 {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) and (max-width: 1263px) {
|
||||||
|
.v-col-md-4 {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-col-md-8 {
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 모바일 반응형 */
|
/* 모바일 반응형 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.d-flex.align-center.justify-space-between.flex-wrap > div {
|
.d-flex.align-center.justify-space-between.flex-wrap > div {
|
||||||
@ -800,5 +1053,9 @@ onMounted(async () => {
|
|||||||
.d-flex.align-center.justify-space-between.flex-wrap > div:last-child {
|
.d-flex.align-center.justify-space-between.flex-wrap > div:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,657 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container fluid class="pa-4">
|
|
||||||
<!-- 페이지 헤더 -->
|
|
||||||
<v-row class="mb-4">
|
|
||||||
<v-col cols="12">
|
|
||||||
<div class="d-flex align-center justify-space-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-h4 font-weight-bold mb-2">메뉴 관리</h1>
|
|
||||||
<p class="text-grey">매장의 메뉴를 등록하고 관리할 수 있습니다</p>
|
|
||||||
</div>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
prepend-icon="mdi-plus"
|
|
||||||
@click="openCreateDialog"
|
|
||||||
>
|
|
||||||
메뉴 추가
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 검색 및 필터 -->
|
|
||||||
<v-row class="mb-4">
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="searchQuery"
|
|
||||||
label="메뉴 검색"
|
|
||||||
prepend-inner-icon="mdi-magnify"
|
|
||||||
variant="outlined"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="selectedCategory"
|
|
||||||
label="카테고리"
|
|
||||||
variant="outlined"
|
|
||||||
:items="categories"
|
|
||||||
clearable
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-select
|
|
||||||
v-model="sortBy"
|
|
||||||
label="정렬"
|
|
||||||
variant="outlined"
|
|
||||||
:items="sortOptions"
|
|
||||||
hide-details
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 메뉴가 없는 경우 -->
|
|
||||||
<div v-if="filteredMenus.length === 0 && !menuStore.loading">
|
|
||||||
<v-row justify="center">
|
|
||||||
<v-col cols="12" md="8">
|
|
||||||
<v-card class="text-center pa-8" elevation="4">
|
|
||||||
<v-img
|
|
||||||
src="/images/menu-placeholder.png"
|
|
||||||
max-width="200"
|
|
||||||
class="mx-auto mb-4"
|
|
||||||
/>
|
|
||||||
<h2 class="text-h5 font-weight-bold mb-3">첫 메뉴를 등록해보세요!</h2>
|
|
||||||
<p class="text-grey mb-4">
|
|
||||||
메뉴를 등록하면 AI가 더 정확한 마케팅 콘텐츠를 생성할 수 있습니다
|
|
||||||
</p>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
prepend-icon="mdi-food-apple"
|
|
||||||
@click="openCreateDialog"
|
|
||||||
>
|
|
||||||
메뉴 등록하기
|
|
||||||
</v-btn>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 메뉴 목록 -->
|
|
||||||
<div v-else>
|
|
||||||
<!-- 메뉴 통계 -->
|
|
||||||
<v-row class="mb-4">
|
|
||||||
<v-col cols="6" sm="3">
|
|
||||||
<v-card elevation="2" class="text-center pa-4">
|
|
||||||
<h3 class="text-h4 font-weight-bold text-primary">{{ menuStore.totalCount }}</h3>
|
|
||||||
<p class="text-caption text-grey">전체 메뉴</p>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="6" sm="3">
|
|
||||||
<v-card elevation="2" class="text-center pa-4">
|
|
||||||
<h3 class="text-h4 font-weight-bold text-success">{{ availableMenuCount }}</h3>
|
|
||||||
<p class="text-caption text-grey">판매중</p>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="6" sm="3">
|
|
||||||
<v-card elevation="2" class="text-center pa-4">
|
|
||||||
<h3 class="text-h4 font-weight-bold text-info">{{ categoriesCount }}</h3>
|
|
||||||
<p class="text-caption text-grey">카테고리</p>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="6" sm="3">
|
|
||||||
<v-card elevation="2" class="text-center pa-4">
|
|
||||||
<h3 class="text-h4 font-weight-bold text-warning">{{ formatCurrency(averagePrice) }}</h3>
|
|
||||||
<p class="text-caption text-grey">평균 가격</p>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 메뉴 그리드 -->
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="menu in filteredMenus"
|
|
||||||
:key="menu.id"
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
lg="3"
|
|
||||||
>
|
|
||||||
<v-card elevation="2" class="h-100">
|
|
||||||
<!-- 메뉴 이미지 -->
|
|
||||||
<v-img
|
|
||||||
:src="menu.imageUrl || '/images/menu-placeholder.png'"
|
|
||||||
:alt="menu.menuName"
|
|
||||||
height="200"
|
|
||||||
cover
|
|
||||||
>
|
|
||||||
<div class="d-flex pa-2">
|
|
||||||
<v-spacer />
|
|
||||||
<v-chip
|
|
||||||
:color="menu.available ? 'success' : 'error'"
|
|
||||||
size="small"
|
|
||||||
variant="elevated"
|
|
||||||
>
|
|
||||||
{{ menu.available ? '판매중' : '품절' }}
|
|
||||||
</v-chip>
|
|
||||||
</div>
|
|
||||||
</v-img>
|
|
||||||
|
|
||||||
<!-- 메뉴 정보 -->
|
|
||||||
<v-card-text class="pa-4">
|
|
||||||
<div class="d-flex align-center justify-space-between mb-2">
|
|
||||||
<v-chip
|
|
||||||
:color="getCategoryColor(menu.category)"
|
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
|
||||||
>
|
|
||||||
{{ menu.category }}
|
|
||||||
</v-chip>
|
|
||||||
<div class="d-flex">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click="editMenu(menu)"
|
|
||||||
>
|
|
||||||
<v-icon size="20">mdi-pencil</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
color="error"
|
|
||||||
@click="confirmDelete(menu)"
|
|
||||||
>
|
|
||||||
<v-icon size="20">mdi-delete</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-h6 font-weight-bold mb-2">{{ menu.menuName }}</h3>
|
|
||||||
<p class="text-body-2 text-grey mb-3" style="min-height: 40px;">
|
|
||||||
{{ menu.description || '메뉴 설명이 없습니다' }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="d-flex align-center justify-space-between">
|
|
||||||
<span class="text-h6 font-weight-bold text-primary">
|
|
||||||
{{ formatCurrency(menu.price) }}
|
|
||||||
</span>
|
|
||||||
<v-rating
|
|
||||||
:model-value="menu.rating || 0"
|
|
||||||
readonly
|
|
||||||
size="small"
|
|
||||||
color="warning"
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 메뉴 등록/수정 다이얼로그 -->
|
|
||||||
<v-dialog
|
|
||||||
v-model="showMenuDialog"
|
|
||||||
max-width="600"
|
|
||||||
persistent
|
|
||||||
scrollable
|
|
||||||
>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="pa-4">
|
|
||||||
<span class="text-h6">{{ editMode ? '메뉴 수정' : '메뉴 등록' }}</span>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="closeMenuDialog"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<v-card-text class="pa-6" style="max-height: 500px;">
|
|
||||||
<v-form ref="menuForm" v-model="menuFormValid">
|
|
||||||
<!-- 메뉴 이미지 -->
|
|
||||||
<div class="text-center mb-6">
|
|
||||||
<v-img
|
|
||||||
:src="menuFormData.imageUrl || '/images/menu-placeholder.png'"
|
|
||||||
:alt="menuFormData.menuName"
|
|
||||||
max-width="200"
|
|
||||||
max-height="150"
|
|
||||||
class="mx-auto mb-3 rounded"
|
|
||||||
/>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
prepend-icon="mdi-camera"
|
|
||||||
@click="selectMenuImage"
|
|
||||||
>
|
|
||||||
이미지 선택
|
|
||||||
</v-btn>
|
|
||||||
<input
|
|
||||||
ref="menuImageInput"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
style="display: none;"
|
|
||||||
@change="handleMenuImageUpload"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" sm="8">
|
|
||||||
<v-text-field
|
|
||||||
v-model="menuFormData.menuName"
|
|
||||||
label="메뉴명 *"
|
|
||||||
variant="outlined"
|
|
||||||
:rules="[v => !!v || '메뉴명을 입력해주세요']"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="4">
|
|
||||||
<v-text-field
|
|
||||||
v-model.number="menuFormData.price"
|
|
||||||
label="가격 *"
|
|
||||||
variant="outlined"
|
|
||||||
type="number"
|
|
||||||
prefix="₩"
|
|
||||||
:rules="priceRules"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-combobox
|
|
||||||
v-model="menuFormData.category"
|
|
||||||
label="카테고리 *"
|
|
||||||
variant="outlined"
|
|
||||||
:items="categories"
|
|
||||||
:rules="[v => !!v || '카테고리를 선택해주세요']"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-switch
|
|
||||||
v-model="menuFormData.available"
|
|
||||||
label="판매 가능"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-textarea
|
|
||||||
v-model="menuFormData.description"
|
|
||||||
label="메뉴 설명"
|
|
||||||
variant="outlined"
|
|
||||||
rows="3"
|
|
||||||
counter="200"
|
|
||||||
:rules="[v => !v || v.length <= 200 || '200자 이내로 입력해주세요']"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="menuFormData.ingredients"
|
|
||||||
label="주요 재료"
|
|
||||||
variant="outlined"
|
|
||||||
hint="쉼표로 구분하여 입력"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-select
|
|
||||||
v-model="menuFormData.spicyLevel"
|
|
||||||
label="매운맛 정도"
|
|
||||||
variant="outlined"
|
|
||||||
:items="spicyLevels"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model.number="menuFormData.calories"
|
|
||||||
label="칼로리"
|
|
||||||
variant="outlined"
|
|
||||||
type="number"
|
|
||||||
suffix="kcal"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-switch
|
|
||||||
v-model="menuFormData.recommended"
|
|
||||||
label="추천 메뉴"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
|
|
||||||
<v-card-actions class="pa-4">
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
color="grey"
|
|
||||||
variant="text"
|
|
||||||
@click="closeMenuDialog"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
:loading="saving"
|
|
||||||
:disabled="!menuFormValid"
|
|
||||||
@click="saveMenu"
|
|
||||||
>
|
|
||||||
{{ editMode ? '수정하기' : '등록하기' }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<!-- 삭제 확인 다이얼로그 -->
|
|
||||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="text-h6">메뉴 삭제</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p>정말로 <strong>{{ deleteTarget?.menuName }}</strong> 메뉴를 삭제하시겠습니까?</p>
|
|
||||||
<v-alert type="warning" variant="tonal" class="mt-3">
|
|
||||||
삭제된 메뉴는 복구할 수 없습니다.
|
|
||||||
</v-alert>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
color="grey"
|
|
||||||
variant="text"
|
|
||||||
@click="showDeleteDialog = false"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
color="error"
|
|
||||||
:loading="deleting"
|
|
||||||
@click="deleteMenu"
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<!-- 로딩 오버레이 -->
|
|
||||||
<v-overlay v-if="menuStore.loading" class="align-center justify-center">
|
|
||||||
<v-progress-circular
|
|
||||||
color="primary"
|
|
||||||
indeterminate
|
|
||||||
size="64"
|
|
||||||
/>
|
|
||||||
</v-overlay>
|
|
||||||
|
|
||||||
<!-- 스낵바 -->
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbar.show"
|
|
||||||
:color="snackbar.color"
|
|
||||||
:timeout="3000"
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
{{ snackbar.message }}
|
|
||||||
</v-snackbar>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { useMenuStore, useAppStore } from '@/store/index'
|
|
||||||
import { formatCurrency } from '@/utils/formatters'
|
|
||||||
|
|
||||||
const menuStore = useMenuStore()
|
|
||||||
const appStore = useAppStore()
|
|
||||||
|
|
||||||
// 반응형 데이터
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const selectedCategory = ref('')
|
|
||||||
const sortBy = ref('name')
|
|
||||||
const showMenuDialog = ref(false)
|
|
||||||
const showDeleteDialog = ref(false)
|
|
||||||
const editMode = ref(false)
|
|
||||||
const menuFormValid = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
const deleting = ref(false)
|
|
||||||
const deleteTarget = ref(null)
|
|
||||||
const menuImageInput = ref(null)
|
|
||||||
|
|
||||||
const snackbar = reactive({
|
|
||||||
show: false,
|
|
||||||
message: '',
|
|
||||||
color: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 메뉴 폼 데이터
|
|
||||||
const menuFormData = reactive({
|
|
||||||
menuName: '',
|
|
||||||
price: 0,
|
|
||||||
category: '',
|
|
||||||
description: '',
|
|
||||||
ingredients: '',
|
|
||||||
spicyLevel: '보통',
|
|
||||||
calories: 0,
|
|
||||||
available: true,
|
|
||||||
recommended: false,
|
|
||||||
imageUrl: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 옵션 데이터
|
|
||||||
const categories = computed(() => {
|
|
||||||
const menuCategories = menuStore.menus.map(menu => menu.category)
|
|
||||||
const defaultCategories = ['면류', '튀김류', '음료', '안주', '디저트']
|
|
||||||
return [...new Set([...defaultCategories, ...menuCategories])]
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortOptions = [
|
|
||||||
{ title: '이름순', value: 'name' },
|
|
||||||
{ title: '가격 낮은순', value: 'price_asc' },
|
|
||||||
{ title: '가격 높은순', value: 'price_desc' },
|
|
||||||
{ title: '추천순', value: 'recommended' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const spicyLevels = ['안매움', '조금매움', '보통', '매움', '아주매움']
|
|
||||||
|
|
||||||
// 유효성 검사 규칙
|
|
||||||
const priceRules = [
|
|
||||||
v => !!v || '가격을 입력해주세요',
|
|
||||||
v => v > 0 || '0원보다 큰 가격을 입력해주세요'
|
|
||||||
]
|
|
||||||
|
|
||||||
// 컴퓨티드 속성
|
|
||||||
const filteredMenus = computed(() => {
|
|
||||||
let filtered = menuStore.menus
|
|
||||||
|
|
||||||
// 검색 필터
|
|
||||||
if (searchQuery.value) {
|
|
||||||
filtered = filtered.filter(menu =>
|
|
||||||
menu.menuName.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 카테고리 필터
|
|
||||||
if (selectedCategory.value) {
|
|
||||||
filtered = filtered.filter(menu => menu.category === selectedCategory.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정렬
|
|
||||||
switch (sortBy.value) {
|
|
||||||
case 'name':
|
|
||||||
filtered.sort((a, b) => a.menuName.localeCompare(b.menuName))
|
|
||||||
break
|
|
||||||
case 'price_asc':
|
|
||||||
filtered.sort((a, b) => a.price - b.price)
|
|
||||||
break
|
|
||||||
case 'price_desc':
|
|
||||||
filtered.sort((a, b) => b.price - a.price)
|
|
||||||
break
|
|
||||||
case 'recommended':
|
|
||||||
filtered.sort((a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
})
|
|
||||||
|
|
||||||
const availableMenuCount = computed(() =>
|
|
||||||
menuStore.menus.filter(menu => menu.available).length
|
|
||||||
)
|
|
||||||
|
|
||||||
const categoriesCount = computed(() =>
|
|
||||||
new Set(menuStore.menus.map(menu => menu.category)).size
|
|
||||||
)
|
|
||||||
|
|
||||||
const averagePrice = computed(() => {
|
|
||||||
if (menuStore.menus.length === 0) return 0
|
|
||||||
const total = menuStore.menus.reduce((sum, menu) => sum + menu.price, 0)
|
|
||||||
return Math.round(total / menuStore.menus.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 메서드
|
|
||||||
const getCategoryColor = (category) => {
|
|
||||||
const colors = {
|
|
||||||
'면류': 'orange',
|
|
||||||
'튀김류': 'red',
|
|
||||||
'음료': 'blue',
|
|
||||||
'안주': 'purple',
|
|
||||||
'디저트': 'pink'
|
|
||||||
}
|
|
||||||
return colors[category] || 'grey'
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
|
||||||
editMode.value = false
|
|
||||||
resetMenuForm()
|
|
||||||
showMenuDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const editMenu = (menu) => {
|
|
||||||
editMode.value = true
|
|
||||||
Object.assign(menuFormData, menu)
|
|
||||||
showMenuDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenuDialog = () => {
|
|
||||||
showMenuDialog.value = false
|
|
||||||
resetMenuForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetMenuForm = () => {
|
|
||||||
Object.assign(menuFormData, {
|
|
||||||
menuName: '',
|
|
||||||
price: 0,
|
|
||||||
category: '',
|
|
||||||
description: '',
|
|
||||||
ingredients: '',
|
|
||||||
spicyLevel: '보통',
|
|
||||||
calories: 0,
|
|
||||||
available: true,
|
|
||||||
recommended: false,
|
|
||||||
imageUrl: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMenuImage = () => {
|
|
||||||
menuImageInput.value?.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMenuImageUpload = (event) => {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
menuFormData.imageUrl = e.target.result
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMenu = async () => {
|
|
||||||
if (!menuFormValid.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
saving.value = true
|
|
||||||
|
|
||||||
if (editMode.value) {
|
|
||||||
await menuStore.updateMenu(menuFormData.id, menuFormData)
|
|
||||||
snackbar.message = '메뉴가 수정되었습니다'
|
|
||||||
} else {
|
|
||||||
await menuStore.createMenu(menuFormData)
|
|
||||||
snackbar.message = '메뉴가 등록되었습니다'
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.color = 'success'
|
|
||||||
snackbar.show = true
|
|
||||||
closeMenuDialog()
|
|
||||||
} catch (error) {
|
|
||||||
snackbar.message = error.response?.data?.message || '저장 중 오류가 발생했습니다'
|
|
||||||
snackbar.color = 'error'
|
|
||||||
snackbar.show = true
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = (menu) => {
|
|
||||||
deleteTarget.value = menu
|
|
||||||
showDeleteDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMenu = async () => {
|
|
||||||
if (!deleteTarget.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
deleting.value = true
|
|
||||||
await menuStore.deleteMenu(deleteTarget.value.id)
|
|
||||||
|
|
||||||
snackbar.message = '메뉴가 삭제되었습니다'
|
|
||||||
snackbar.color = 'success'
|
|
||||||
snackbar.show = true
|
|
||||||
|
|
||||||
showDeleteDialog.value = false
|
|
||||||
deleteTarget.value = null
|
|
||||||
} catch (error) {
|
|
||||||
snackbar.message = error.response?.data?.message || '삭제 중 오류가 발생했습니다'
|
|
||||||
snackbar.color = 'error'
|
|
||||||
snackbar.show = true
|
|
||||||
} finally {
|
|
||||||
deleting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 라이프사이클
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
await menuStore.fetchMenus()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('메뉴 목록 로드 실패:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.h-100 {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.text-h4 {
|
|
||||||
font-size: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -586,13 +586,13 @@
|
|||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- SNS 정보 -->
|
<!-- SNS 정보 섹션 -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 class="text-h6 font-weight-bold mb-4">SNS 계정 정보</h3>
|
<h3 class="text-h6 font-weight-bold mb-4">SNS 계정 정보</h3>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<!-- 인스타그램 -->
|
<!-- 인스타그램 -->
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12">
|
||||||
<div class="d-flex align-center mb-2">
|
<div class="d-flex align-center mb-2">
|
||||||
<v-icon color="purple" class="mr-2">mdi-instagram</v-icon>
|
<v-icon color="purple" class="mr-2">mdi-instagram</v-icon>
|
||||||
<span class="text-subtitle-2 font-weight-medium">인스타그램</span>
|
<span class="text-subtitle-2 font-weight-medium">인스타그램</span>
|
||||||
@ -607,13 +607,14 @@
|
|||||||
prepend-inner-icon="mdi-at"
|
prepend-inner-icon="mdi-at"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
class="flex-grow-1"
|
class="flex-grow-1"
|
||||||
|
style="min-width: 300px;"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="purple"
|
color="purple"
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:loading="snsCheckLoading.instagram"
|
:loading="snsCheckLoading.instagram"
|
||||||
@click="checkSnsConnection('instagram')"
|
@click="checkSnsConnection('instagram')"
|
||||||
|
style="min-width: 80px; flex-shrink: 0;"
|
||||||
>
|
>
|
||||||
연동 확인
|
연동 확인
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -621,28 +622,29 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- 네이버 블로그 -->
|
<!-- 네이버 블로그 -->
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12">
|
||||||
<div class="d-flex align-center mb-2">
|
<div class="d-flex align-center mb-2">
|
||||||
<v-icon color="green" class="mr-2">mdi-blogger</v-icon>
|
<v-icon color="green" class="mr-2">mdi-web</v-icon>
|
||||||
<span class="text-subtitle-2 font-weight-medium">네이버 블로그</span>
|
<span class="text-subtitle-2 font-weight-medium">네이버 블로그</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2 align-center">
|
<div class="d-flex gap-2 align-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.blogUrl"
|
v-model="formData.blogUrl"
|
||||||
placeholder="blog.naver.com/계정명"
|
placeholder="blog.naver.com/계정명 형식으로 입력"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
prepend-inner-icon="mdi-web"
|
prepend-inner-icon="mdi-web"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
class="flex-grow-1"
|
class="flex-grow-1"
|
||||||
|
style="min-width: 300px;"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="green"
|
color="green"
|
||||||
size="small"
|
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:loading="snsCheckLoading.naver_blog"
|
:loading="snsCheckLoading.blog"
|
||||||
@click="checkSnsConnection('naver_blog')"
|
@click="checkSnsConnection('blog')"
|
||||||
|
style="min-width: 80px; flex-shrink: 0;"
|
||||||
>
|
>
|
||||||
연동 확인
|
연동 확인
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user