smarketing-frontend/src/views/AIRecommendationView.vue
2025-06-11 14:17:26 +09:00

854 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//* src/views/AIRecommendationView.vue
<template>
<v-container fluid class="pa-4">
<!-- AI 추천 대시보드 -->
<v-row class="mb-4">
<v-col cols="12">
<v-card
class="pa-6"
elevation="2"
color="gradient-primary"
>
<v-row align="center">
<v-col cols="12" md="8">
<div class="d-flex align-center mb-3">
<v-avatar
size="64"
color="white"
class="mr-4"
>
<v-icon size="32" color="primary">mdi-robot</v-icon>
</v-avatar>
<div>
<h2 class="text-h4 font-weight-bold white--text">
AI 마케팅 어시스턴트
</h2>
<p class="text-h6 white--text opacity-90">
데이터 기반 맞춤형 마케팅 전략을 제공합니다
</p>
</div>
</div>
</v-col>
<v-col cols="12" md="4" class="text-center">
<v-btn
size="large"
color="white"
class="text-primary"
@click="generateAllRecommendations"
:loading="generatingAll"
>
<v-icon class="mr-2">mdi-refresh</v-icon>
전체 추천 새로고침
</v-btn>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<!-- 오늘의 추천 카드 -->
<v-row class="mb-4">
<v-col cols="12">
<v-card elevation="2">
<v-card-title class="text-h6 pa-4">
<v-icon class="mr-2" color="warning">mdi-star</v-icon>
오늘의 특별 추천
<v-spacer />
<v-chip color="success" size="small">
{{ getCurrentDate() }}
</v-chip>
</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<v-card
v-if="todayRecommendation"
class="pa-4 mb-4"
color="amber-lighten-5"
variant="tonal"
>
<v-row align="center">
<v-col cols="12" md="8">
<div class="d-flex align-center mb-2">
<v-icon color="amber-darken-2" class="mr-2">mdi-lightbulb</v-icon>
<h4 class="text-h6 font-weight-bold">
{{ todayRecommendation.title }}
</h4>
</div>
<p class="text-body-1 mb-3">
{{ todayRecommendation.content }}
</p>
<div class="d-flex flex-wrap gap-2">
<v-chip
v-for="tag in todayRecommendation.tags"
:key="tag"
size="small"
color="amber"
>
{{ tag }}
</v-chip>
</div>
</v-col>
<v-col cols="12" md="4" class="text-center">
<v-btn
color="primary"
size="large"
@click="applyRecommendation(todayRecommendation)"
>
<v-icon class="mr-1">mdi-check-circle</v-icon>
추천 적용하기
</v-btn>
</v-col>
</v-row>
</v-card>
<div class="text-right">
<v-btn
color="primary"
variant="outlined"
size="small"
@click="generateTodayRecommendation"
:loading="generatingToday"
>
<v-icon class="mr-1">mdi-refresh</v-icon>
추천 받기
</v-btn>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 추천 카테고리 -->
<v-row class="mb-4">
<v-col cols="12">
<v-card elevation="2">
<v-card-title class="text-h6 pa-4">
<v-icon class="mr-2" color="info">mdi-view-grid</v-icon>
AI 추천 카테고리
</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<v-row>
<v-col
v-for="category in recommendationCategories"
:key="category.id"
cols="12"
sm="6"
md="4"
>
<v-card
class="recommendation-category-card"
:class="{ 'selected': selectedCategory === category.id }"
@click="selectCategory(category.id)"
hover
>
<v-card-text class="text-center pa-6">
<v-icon
:color="category.color"
size="48"
class="mb-3"
>
{{ category.icon }}
</v-icon>
<h4 class="text-h6 font-weight-bold mb-2">
{{ category.title }}
</h4>
<p class="text-body-2 grey--text">
{{ category.description }}
</p>
<v-chip
:color="category.color"
size="small"
class="mt-2"
>
{{ category.count }} 추천
</v-chip>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 선택된 카테고리 추천 목록 -->
<v-row v-if="selectedCategory">
<v-col cols="12">
<v-card elevation="2">
<v-card-title class="text-h6 pa-4">
<v-icon class="mr-2" :color="getCategoryInfo(selectedCategory).color">
{{ getCategoryInfo(selectedCategory).icon }}
</v-icon>
{{ getCategoryInfo(selectedCategory).title }} 추천
<v-spacer />
<v-btn
color="primary"
variant="outlined"
size="small"
@click="generateCategoryRecommendations(selectedCategory)"
:loading="generatingCategory"
>
<v-icon class="mr-1">mdi-refresh</v-icon>
새로고침
</v-btn>
</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<v-row v-if="categoryRecommendations.length > 0">
<v-col
v-for="(recommendation, index) in categoryRecommendations"
:key="index"
cols="12"
md="6"
>
<v-card
class="recommendation-item-card"
elevation="1"
hover
>
<v-card-text class="pa-4">
<div class="d-flex align-start mb-3">
<v-avatar
size="40"
:color="recommendation.priority === 'high' ? 'error' :
recommendation.priority === 'medium' ? 'warning' : 'success'"
class="mr-3"
>
<span class="white--text font-weight-bold">
{{ index + 1 }}
</span>
</v-avatar>
<div class="flex-grow-1">
<h4 class="text-h6 font-weight-bold mb-1">
{{ recommendation.title }}
</h4>
<p class="text-body-2 grey--text mb-2">
{{ recommendation.description }}
</p>
</div>
</div>
<!-- 추천 이유 -->
<v-card
class="pa-3 mb-3"
color="blue-grey-lighten-5"
variant="tonal"
>
<div class="text-caption grey--text mb-1">추천 이유</div>
<div class="text-body-2">{{ recommendation.reason }}</div>
</v-card>
<!-- 예상 효과 -->
<div class="mb-3">
<div class="text-caption grey--text mb-1">예상 효과</div>
<v-chip
v-for="effect in recommendation.expectedEffects"
:key="effect"
size="small"
color="green"
class="mr-1 mb-1"
>
{{ effect }}
</v-chip>
</div>
<!-- 우선순위 -->
<div class="d-flex justify-space-between align-center">
<v-chip
:color="recommendation.priority === 'high' ? 'error' :
recommendation.priority === 'medium' ? 'warning' : 'success'"
size="small"
>
{{ getPriorityText(recommendation.priority) }}
</v-chip>
<v-chip
color="info"
size="small"
>
예상 소요시간: {{ recommendation.estimatedTime }}
</v-chip>
</div>
</v-card-text>
<v-card-actions class="pt-0 px-4 pb-4">
<v-btn
color="primary"
variant="outlined"
size="small"
@click="viewRecommendationDetail(recommendation)"
>
상세보기
</v-btn>
<v-btn
color="success"
size="small"
@click="applyRecommendation(recommendation)"
>
적용하기
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<!-- 로딩 상태 -->
<v-card
v-else-if="generatingCategory"
class="pa-8 text-center"
>
<v-progress-circular
indeterminate
color="primary"
size="64"
class="mb-4"
/>
<p class="text-body-1">AI가 추천을 분석하고 있습니다...</p>
</v-card>
<!-- 상태 -->
<v-card
v-else
class="pa-6 text-center"
color="grey-lighten-4"
variant="tonal"
>
<v-icon size="48" color="grey" class="mb-2">mdi-robot-outline</v-icon>
<p class="text-body-2">해당 카테고리의 추천을 생성해주세요</p>
</v-card>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 추천 상세 다이얼로그 -->
<v-dialog
v-model="showDetailDialog"
max-width="700"
scrollable
>
<v-card v-if="selectedRecommendation">
<v-card-title class="text-h6">
{{ selectedRecommendation.title }}
<v-spacer />
<v-btn
icon
@click="showDetailDialog = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<!-- 기본 정보 -->
<div class="mb-4">
<h4 class="text-h6 mb-2">📋 추천 개요</h4>
<p class="text-body-1">{{ selectedRecommendation.description }}</p>
</div>
<!-- 추천 이유 -->
<div class="mb-4">
<h4 class="text-h6 mb-2">🔍 추천 이유</h4>
<v-card
class="pa-3"
color="blue-grey-lighten-5"
variant="tonal"
>
<p class="text-body-1">{{ selectedRecommendation.reason }}</p>
</v-card>
</div>
<!-- 실행 단계 -->
<div class="mb-4" v-if="selectedRecommendation.steps">
<h4 class="text-h6 mb-2">📝 실행 단계</h4>
<v-list>
<v-list-item
v-for="(step, index) in selectedRecommendation.steps"
:key="index"
class="px-0"
>
<template v-slot:prepend>
<v-avatar
size="32"
color="primary"
>
<span class="white--text font-weight-bold">
{{ index + 1 }}
</span>
</v-avatar>
</template>
<v-list-item-title>{{ step }}</v-list-item-title>
</v-list-item>
</v-list>
</div>
<!-- 예상 효과 -->
<div class="mb-4">
<h4 class="text-h6 mb-2">📈 예상 효과</h4>
<v-chip
v-for="effect in selectedRecommendation.expectedEffects"
:key="effect"
color="success"
class="mr-1 mb-1"
>
{{ effect }}
</v-chip>
</div>
<!-- 관련 데이터 -->
<div class="mb-4" v-if="selectedRecommendation.relatedData">
<h4 class="text-h6 mb-2">📊 관련 데이터</h4>
<v-card
class="pa-3"
color="info"
variant="tonal"
>
<div
v-for="(data, key) in selectedRecommendation.relatedData"
:key="key"
class="d-flex justify-space-between mb-1"
>
<span>{{ key }}:</span>
<strong>{{ data }}</strong>
</div>
</v-card>
</div>
<!-- 주의사항 -->
<div v-if="selectedRecommendation.warnings">
<h4 class="text-h6 mb-2"> 주의사항</h4>
<v-alert
color="warning"
variant="tonal"
icon="mdi-alert"
>
<ul class="pl-4">
<li
v-for="warning in selectedRecommendation.warnings"
:key="warning"
>
{{ warning }}
</li>
</ul>
</v-alert>
</div>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-btn
color="success"
@click="applyRecommendation(selectedRecommendation)"
>
<v-icon class="mr-1">mdi-check</v-icon>
추천 적용하기
</v-btn>
<v-btn
color="primary"
variant="outlined"
@click="saveRecommendation(selectedRecommendation)"
>
<v-icon class="mr-1">mdi-bookmark</v-icon>
나중에 하기
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 성공 스낵바 -->
<v-snackbar
v-model="showSuccess"
color="success"
timeout="3000"
>
{{ successMessage }}
</v-snackbar>
<!-- 에러 스낵바 -->
<v-snackbar
v-model="showError"
color="error"
timeout="3000"
>
{{ errorMessage }}
</v-snackbar>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import * as recommendService from '@/services/recommend'
const router = useRouter()
// 상태 관리
const selectedCategory = ref('')
const todayRecommendation = ref(null)
const categoryRecommendations = ref([])
const selectedRecommendation = ref(null)
const generatingAll = ref(false)
const generatingToday = ref(false)
const generatingCategory = ref(false)
const showDetailDialog = ref(false)
const showSuccess = ref(false)
const showError = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
// 추천 카테고리 설정
const recommendationCategories = ref([
{
id: 'content',
title: '콘텐츠 마케팅',
description: 'SNS 게시물 및 홍보 포스터 아이디어',
icon: 'mdi-pencil-plus',
color: 'purple',
count: 5
},
{
id: 'promotion',
title: '프로모션 전략',
description: '할인 이벤트 및 특가 제안',
icon: 'mdi-sale',
color: 'orange',
count: 4
},
{
id: 'menu',
title: '메뉴 최적화',
description: '인기 메뉴 분석 및 신메뉴 제안',
icon: 'mdi-food',
color: 'green',
count: 6
},
{
id: 'customer',
title: '고객 관리',
description: '고객 만족도 향상 방안',
icon: 'mdi-account-heart',
color: 'pink',
count: 3
},
{
id: 'timing',
title: '타이밍 전략',
description: '최적 홍보 시점 및 계절 마케팅',
icon: 'mdi-clock',
color: 'blue',
count: 4
},
{
id: 'analysis',
title: '데이터 분석',
description: '매출 분석 및 트렌드 인사이트',
icon: 'mdi-chart-line',
color: 'teal',
count: 5
}
])
/**
* 현재 날짜 반환
*/
const getCurrentDate = () => {
return new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
}
/**
* 카테고리 정보 반환
*/
const getCategoryInfo = (categoryId) => {
return recommendationCategories.value.find(cat => cat.id === categoryId) || {}
}
/**
* 우선순위 텍스트 반환
*/
const getPriorityText = (priority) => {
const priorities = {
high: '높음',
medium: '보통',
low: '낮음'
}
return priorities[priority] || priority
}
/**
* 카테고리 선택
*/
const selectCategory = async (categoryId) => {
selectedCategory.value = categoryId
await generateCategoryRecommendations(categoryId)
}
/**
* 오늘의 추천 생성
*/
const generateTodayRecommendation = async () => {
generatingToday.value = true
try {
const response = await recommendService.generateMarketingTips({
type: 'daily',
includeWeather: true,
includeTrends: true
})
todayRecommendation.value = response
} catch (error) {
console.error('오늘의 추천 생성 실패:', error)
// 샘플 데이터로 대체
todayRecommendation.value = {
title: '오늘은 날씨가 좋으니 테이크아웃 마케팅을 강화하세요!',
content: '맑은 날씨로 인해 야외 활동이 증가할 것으로 예상됩니다. 테이크아웃 전용 할인이나 피크닉 세트 메뉴를 홍보하면 좋은 반응을 얻을 수 있을 것입니다.',
tags: ['날씨연동', '테이크아웃', '야외활동', '피크닉']
}
} finally {
generatingToday.value = false
}
}
/**
* 카테고리별 추천 생성
*/
const generateCategoryRecommendations = async (categoryId) => {
generatingCategory.value = true
try {
const response = await recommendService.generateMarketingTips({
type: 'category',
category: categoryId,
limit: 6
})
categoryRecommendations.value = response
} catch (error) {
console.error('카테고리 추천 생성 실패:', error)
// 샘플 데이터로 대체
categoryRecommendations.value = getSampleRecommendations(categoryId)
} finally {
generatingCategory.value = false
}
}
/**
* 전체 추천 새로고침
*/
const generateAllRecommendations = async () => {
generatingAll.value = true
try {
await generateTodayRecommendation()
if (selectedCategory.value) {
await generateCategoryRecommendations(selectedCategory.value)
}
successMessage.value = '모든 추천이 새로고침되었습니다.'
showSuccess.value = true
} catch (error) {
console.error('전체 추천 새로고침 실패:', error)
errorMessage.value = '추천 새로고침 중 오류가 발생했습니다.'
showError.value = true
} finally {
generatingAll.value = false
}
}
/**
* 추천 상세보기
*/
const viewRecommendationDetail = (recommendation) => {
selectedRecommendation.value = recommendation
showDetailDialog.value = true
}
/**
* 추천 적용
*/
const applyRecommendation = async (recommendation) => {
try {
// 추천 타입에 따라 다른 페이지로 이동
if (recommendation.type === 'content' || recommendation.title.includes('콘텐츠')) {
router.push({ name: 'ContentCreation' })
} else if (recommendation.type === 'menu' || recommendation.title.includes('메뉴')) {
router.push({ name: 'MenuManagement' })
} else {
// 기본적으로 대시보드로 이동
router.push({ name: 'Dashboard' })
}
successMessage.value = '추천이 적용되었습니다!'
showSuccess.value = true
showDetailDialog.value = false
} catch (error) {
console.error('추천 적용 실패:', error)
errorMessage.value = '추천 적용 중 오류가 발생했습니다.'
showError.value = true
}
}
/**
* 추천 저장
*/
const saveRecommendation = async (recommendation) => {
try {
// 실제로는 추천 저장 API 호출
successMessage.value = '추천이 저장되었습니다. 나중에 확인해보세요.'
showSuccess.value = true
showDetailDialog.value = false
} catch (error) {
console.error('추천 저장 실패:', error)
errorMessage.value = '추천 저장 중 오류가 발생했습니다.'
showError.value = true
}
}
/**
* 샘플 추천 데이터 생성
*/
const getSampleRecommendations = (categoryId) => {
const samples = {
content: [
{
title: '인스타그램 스토리 활용 강화',
description: '일상적인 요리 과정을 스토리로 공유하여 고객과의 친밀감을 높이세요',
reason: '스토리는 24시간 후 사라지는 특성으로 인해 부담 없이 자주 업로드할 수 있고, 고객들의 참여도가 높습니다',
priority: 'high',
estimatedTime: '30분/일',
expectedEffects: ['브랜드 인지도 상승', '고객 참여도 증가', '일상적 소통 증가'],
steps: [
'매일 요리 과정 1-2장 촬영',
'간단한 텍스트와 함께 스토리 업로드',
'고객 댓글에 적극적으로 반응',
'주간 결산 스토리 하이라이트로 저장'
],
type: 'content'
},
{
title: '고객 리뷰 기반 콘텐츠 제작',
description: '긍정적인 고객 리뷰를 활용하여 신뢰도 높은 홍보 콘텐츠를 만드세요',
reason: '실제 고객의 생생한 후기는 잠재 고객들에게 강한 신뢰감을 줍니다',
priority: 'medium',
estimatedTime: '1시간/주',
expectedEffects: ['신뢰도 향상', '구매 의사결정 도움', 'WOM 효과'],
type: 'content'
}
],
promotion: [
{
title: '우천 시 특별 할인 이벤트',
description: '비 오는 날 방문 고객에게 특별 할인 혜택을 제공하세요',
reason: '비 오는 날은 외출을 꺼리는 고객들에게 방문 동기를 부여할 수 있습니다',
priority: 'high',
estimatedTime: '2시간',
expectedEffects: ['비 오는 날 매출 증대', '고객 만족도 상승', '재방문 유도'],
relatedData: {
'우천 시 평균 매출 감소율': '-23%',
'할인 이벤트 시 예상 증가율': '+15%',
'타 업체 유사 이벤트 성공률': '78%'
},
type: 'promotion'
}
],
menu: [
{
title: '계절 한정 메뉴 개발',
description: '여름철에 어울리는 시원한 메뉴나 음료를 개발해보세요',
reason: '계절감 있는 메뉴는 고객들의 관심을 끌고 화제성을 만들 수 있습니다',
priority: 'medium',
estimatedTime: '1주일',
expectedEffects: ['신규 고객 유입', '매출 다양성 확보', '브랜드 이미지 개선'],
warnings: [
'계절 메뉴는 재료 수급이 불안정할 수 있습니다',
'초기 테스트 기간을 거쳐 고객 반응을 확인하세요'
],
type: 'menu'
}
],
customer: [
{
title: '단골 고객 VIP 프로그램 운영',
description: '자주 방문하는 고객들을 위한 특별 혜택 프로그램을 만드세요',
reason: '단골 고객의 재방문율을 높이고 신규 고객의 단골화를 유도할 수 있습니다',
priority: 'high',
estimatedTime: '반나절',
expectedEffects: ['고객 충성도 향상', '재방문율 증가', '구전 효과'],
type: 'customer'
}
],
timing: [
{
title: '점심시간 사전 예약 시스템',
description: '바쁜 점심시간에 미리 주문받아 대기시간을 단축하세요',
reason: '직장인들의 짧은 점심시간을 고려한 서비스로 고객 만족도를 높일 수 있습니다',
priority: 'medium',
estimatedTime: '3시간',
expectedEffects: ['점심시간 매출 증대', '고객 만족도 상승', '운영 효율성 향상'],
type: 'timing'
}
],
analysis: [
{
title: '주간 매출 패턴 분석 리포트',
description: '요일별, 시간대별 매출 데이터를 분석하여 운영 최적화 방안을 찾으세요',
reason: '데이터 기반의 의사결정으로 효율적인 매장 운영이 가능합니다',
priority: 'medium',
estimatedTime: '2시간/주',
expectedEffects: ['운영 효율성 향상', '재고 관리 최적화', '인력 배치 개선'],
type: 'analysis'
}
]
}
return samples[categoryId] || []
}
// 컴포넌트 마운트시 초기 데이터 로드
onMounted(async () => {
await generateTodayRecommendation()
})
</script>
<style scoped>
.gradient-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.recommendation-category-card {
transition: all 0.3s;
cursor: pointer;
border: 2px solid transparent;
}
.recommendation-category-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.recommendation-category-card.selected {
border-color: #1976D2;
background-color: #E3F2FD;
}
.recommendation-item-card {
transition: all 0.3s;
height: 100%;
}
.recommendation-item-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
</style>