release
This commit is contained in:
parent
a5aaff660c
commit
209b52c924
@ -1,854 +0,0 @@
|
|||||||
//* 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>
|
|
||||||
@ -1,40 +1,50 @@
|
|||||||
//* src/views/DashboardView.vue
|
//* src/views/DashboardView.vue
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
<!-- 환영 메시지 -->
|
<!-- 페이지 헤더 -->
|
||||||
<v-row class="mb-4">
|
<div class="d-flex align-center justify-between mb-6">
|
||||||
<v-col cols="12">
|
<div>
|
||||||
<v-card class="bg-gradient-primary" elevation="4">
|
<h1 class="text-h4 font-weight-bold text-grey-darken-4">대시보드</h1>
|
||||||
<v-card-text class="pa-6">
|
<p class="text-body-1 text-grey-darken-1 mb-0">
|
||||||
<div class="d-flex align-center">
|
{{ businessName }}의 실시간 현황을 확인하세요
|
||||||
<div class="flex-grow-1">
|
</p>
|
||||||
<h2 class="text-h5 text-white font-weight-bold mb-2">
|
|
||||||
안녕하세요, {{ authStore.user?.name || '사용자' }}님! 👋
|
|
||||||
</h2>
|
|
||||||
<p class="text-white opacity-90 mb-0">오늘도 성공적인 마케팅을 위해 함께해요</p>
|
|
||||||
</div>
|
</div>
|
||||||
<v-img src="/images/ai-character.png" max-width="80" class="ml-4" />
|
<v-chip :color="'success'" variant="flat" class="text-body-2 font-weight-medium">
|
||||||
|
<v-icon start size="16">mdi-circle</v-icon>
|
||||||
|
실시간 업데이트
|
||||||
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 주요 지표 카드 -->
|
<!-- 주요 지표 카드 - 새로운 스타일 적용 (3개 카드) -->
|
||||||
<v-row class="mb-4">
|
<v-row class="mb-6">
|
||||||
<v-col v-for="metric in dashboardMetrics" :key="metric.title" cols="6" md="3">
|
<v-col
|
||||||
<v-card elevation="2" class="h-100">
|
v-for="(metric, index) in dashboardMetrics"
|
||||||
<v-card-text class="pa-4">
|
:key="metric.title"
|
||||||
<div class="d-flex align-center justify-space-between mb-2">
|
cols="12"
|
||||||
<v-icon :color="metric.color" size="24">{{ metric.icon }}</v-icon>
|
sm="4"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
class="metric-card h-100"
|
||||||
|
:class="`metric-card--${metric.color}`"
|
||||||
|
elevation="0"
|
||||||
|
border
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<!-- 카드 헤더 -->
|
||||||
|
<div class="d-flex align-center justify-between mb-3">
|
||||||
|
<div class="metric-icon-wrapper" :class="`bg-${metric.color}`">
|
||||||
|
<v-icon :icon="metric.icon" size="24" :color="`${metric.color}-darken-2`" />
|
||||||
|
</div>
|
||||||
<v-chip
|
<v-chip
|
||||||
:color="
|
:color="
|
||||||
metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'warning'
|
metric.trend === 'up' ? 'success' : metric.trend === 'down' ? 'error' : 'warning'
|
||||||
"
|
"
|
||||||
size="small"
|
size="small"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
|
class="metric-trend"
|
||||||
>
|
>
|
||||||
<v-icon size="16" class="mr-1">
|
<v-icon size="12" class="mr-1">
|
||||||
{{
|
{{
|
||||||
metric.trend === 'up'
|
metric.trend === 'up'
|
||||||
? 'mdi-trending-up'
|
? 'mdi-trending-up'
|
||||||
@ -46,105 +56,132 @@
|
|||||||
{{ metric.change }}
|
{{ metric.change }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-h6 font-weight-bold mb-1">{{ metric.value }}</h3>
|
|
||||||
<p class="text-body-2 text-grey mb-0">{{ metric.title }}</p>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 빠른 액션 -->
|
<!-- 메트릭 값 - 애니메이션 적용 -->
|
||||||
<v-row class="mb-4">
|
<div class="metric-value-wrapper mb-2">
|
||||||
<v-col cols="12">
|
<span
|
||||||
<v-card elevation="2">
|
class="metric-value text-h4 font-weight-bold"
|
||||||
<v-card-title class="pb-2">빠른 액션</v-card-title>
|
:class="`text-${metric.color}-darken-2`"
|
||||||
<v-card-text>
|
:ref="`metricValue${index}`"
|
||||||
<v-row>
|
|
||||||
<v-col v-for="action in quickActions" :key="action.title" cols="6" md="3">
|
|
||||||
<v-card
|
|
||||||
:color="action.color"
|
|
||||||
variant="tonal"
|
|
||||||
class="text-center"
|
|
||||||
@click="action.action"
|
|
||||||
style="cursor: pointer"
|
|
||||||
>
|
>
|
||||||
<v-card-text class="pa-4">
|
{{ animatedValues[index] || '0' }}
|
||||||
<v-icon :color="action.color" size="32" class="mb-2">
|
</span>
|
||||||
{{ action.icon }}
|
</div>
|
||||||
</v-icon>
|
|
||||||
<div class="text-body-2 font-weight-medium">
|
<!-- 메트릭 제목 -->
|
||||||
{{ action.title }}
|
<p class="metric-title text-body-2 text-grey-darken-1 mb-0 font-weight-medium">
|
||||||
|
{{ metric.title }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 추가 정보 -->
|
||||||
|
<div class="metric-detail mt-2">
|
||||||
|
<span class="text-caption text-grey">
|
||||||
|
{{ metric.detail || '전월 대비' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 매출 추이 분석 차트 -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" lg="8">
|
||||||
|
<v-card elevation="0" border class="chart-card h-100">
|
||||||
|
<v-card-title class="pa-6 pb-0">
|
||||||
|
<div class="d-flex align-center justify-between w-100">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-h6 font-weight-bold mb-1">매출 추이 분석</h3>
|
||||||
|
<p class="text-body-2 text-grey-darken-1 mb-0">최근 7일간 매출 현황</p>
|
||||||
|
</div>
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="chartPeriod"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
color="primary"
|
||||||
|
rounded="lg"
|
||||||
|
@update:model-value="updateChart"
|
||||||
|
>
|
||||||
|
<v-btn value="7d" size="small">7일</v-btn>
|
||||||
|
<v-btn value="30d" size="small">30일</v-btn>
|
||||||
|
<v-btn value="90d" size="small">90일</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-6">
|
||||||
|
<div class="sales-chart-container">
|
||||||
|
<!-- Chart.js 차트가 들어갈 영역 -->
|
||||||
|
<canvas id="salesChart" class="sales-chart" style="height: 300px"></canvas>
|
||||||
|
|
||||||
|
<!-- Chart.js 없을 때 표시될 플레이스홀더 -->
|
||||||
|
<div v-if="!chartInitialized" class="chart-placeholder">
|
||||||
|
<v-icon size="64" color="grey-lighten-2">mdi-chart-line</v-icon>
|
||||||
|
<p class="text-grey mt-2 mb-1">매출 추이 차트</p>
|
||||||
|
<p class="text-caption text-grey">Chart.js를 설치하면 실제 차트가 표시됩니다</p>
|
||||||
|
|
||||||
|
<!-- 간단한 가짜 차트 -->
|
||||||
|
<div class="fake-chart mt-4">
|
||||||
|
<div class="fake-chart-bars">
|
||||||
|
<div v-for="(value, i) in fakeChartData" :key="i" class="fake-bar">
|
||||||
|
<div class="fake-bar-fill" :style="{ height: `${value}%` }"></div>
|
||||||
|
<span class="fake-bar-label">{{ i + 1 }}일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- AI 추천 -->
|
<!-- AI 추천 요약 -->
|
||||||
<v-row class="mb-4">
|
<v-col cols="12" lg="4">
|
||||||
<v-col cols="12" md="8">
|
<v-card elevation="0" border class="ai-recommend-card h-100">
|
||||||
<v-card elevation="2">
|
<v-card-title class="pa-6 pb-0">
|
||||||
<v-card-title class="d-flex align-center">
|
<div class="d-flex align-center mb-1">
|
||||||
<v-icon color="primary" class="mr-2">mdi-robot</v-icon>
|
<v-icon color="primary" class="mr-2">mdi-robot</v-icon>
|
||||||
AI 마케팅 추천
|
<h3 class="text-h6 font-weight-bold">AI 추천 활용</h3>
|
||||||
<v-spacer />
|
</div>
|
||||||
<v-btn icon size="small" @click="refreshAIRecommendations" :loading="aiLoading">
|
<p class="text-body-2 text-grey-darken-1 mb-0">맞춤형 마케팅 제안</p>
|
||||||
<v-icon>mdi-refresh</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text class="pa-6">
|
||||||
<div v-if="aiRecommendations.length > 0">
|
<div v-if="aiLoading" class="text-center py-8">
|
||||||
<v-alert
|
<v-progress-circular color="primary" indeterminate size="40" />
|
||||||
v-for="recommendation in aiRecommendations"
|
<p class="text-body-2 text-grey mt-3">AI가 분석 중...</p>
|
||||||
:key="recommendation.title"
|
</div>
|
||||||
:type="recommendation.type"
|
<div v-else>
|
||||||
:icon="recommendation.icon"
|
<div
|
||||||
variant="tonal"
|
v-for="(recommendation, index) in aiRecommendations"
|
||||||
class="mb-3"
|
:key="index"
|
||||||
|
class="ai-recommendation-item mb-4"
|
||||||
>
|
>
|
||||||
<v-alert-title>{{ recommendation.title }}</v-alert-title>
|
<v-alert
|
||||||
|
:type="recommendation.type"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
:icon="recommendation.icon"
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<v-alert-title class="text-subtitle-2 font-weight-bold mb-1">
|
||||||
|
{{ recommendation.title }}
|
||||||
|
</v-alert-title>
|
||||||
|
<div class="text-body-2">
|
||||||
{{ recommendation.content }}
|
{{ recommendation.content }}
|
||||||
|
</div>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center pa-4">
|
|
||||||
<v-icon size="48" color="grey-lighten-2">mdi-robot</v-icon>
|
|
||||||
<p class="text-grey mt-2">AI 추천을 불러오는 중...</p>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- 최근 활동 -->
|
<v-btn
|
||||||
<v-col cols="12" md="4">
|
color="primary"
|
||||||
<v-card elevation="2">
|
variant="outlined"
|
||||||
<v-card-title>최근 활동</v-card-title>
|
block
|
||||||
<v-card-text>
|
size="small"
|
||||||
<div v-if="recentActivities.length > 0">
|
class="mt-2"
|
||||||
<div
|
@click="router.push('/recommend')"
|
||||||
v-for="activity in recentActivities"
|
|
||||||
:key="activity.id"
|
|
||||||
class="d-flex align-center mb-3"
|
|
||||||
>
|
>
|
||||||
<v-avatar :color="activity.color" size="32" class="mr-3">
|
<v-icon start>mdi-lightbulb-on</v-icon>
|
||||||
<v-icon color="white" size="16">{{ activity.icon }}</v-icon>
|
더 많은 추천 보기
|
||||||
</v-avatar>
|
</v-btn>
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="text-body-2 font-weight-medium">
|
|
||||||
{{ activity.title }}
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-grey">
|
|
||||||
{{ formatRelativeTime(activity.timestamp) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center pa-4">
|
|
||||||
<v-icon size="48" color="grey-lighten-2">mdi-history</v-icon>
|
|
||||||
<p class="text-grey mt-2">최근 활동이 없습니다</p>
|
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@ -159,14 +196,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/store/auth' // 수정된 import
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useAppStore } from '@/store/app' // 추가된 import
|
import { useAppStore } from '@/store/app'
|
||||||
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/formatters'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 대시보드 메인 페이지
|
* 대시보드 메인 페이지 - 수정된 버전
|
||||||
|
* - 새로운 카드 스타일 적용
|
||||||
|
* - 숫자 증가 애니메이션 추가
|
||||||
|
* - 빠른 액션 제거
|
||||||
|
* - 매출 추이 분석 차트 추가
|
||||||
|
* - 최근 활동 제거
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -176,78 +218,54 @@ const appStore = useAppStore()
|
|||||||
// 반응형 데이터
|
// 반응형 데이터
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const aiLoading = ref(false)
|
const aiLoading = ref(false)
|
||||||
|
const chartInitialized = ref(false)
|
||||||
|
const chartPeriod = ref('7d')
|
||||||
|
const animatedValues = ref({})
|
||||||
|
|
||||||
// 대시보드 지표
|
// 비즈니스 정보
|
||||||
|
const businessName = ref('김사장님의 분식점')
|
||||||
|
|
||||||
|
// 대시보드 지표 - 새로운 스타일 적용 (AI 추천 활용 카드 제거)
|
||||||
const dashboardMetrics = ref([
|
const dashboardMetrics = ref([
|
||||||
{
|
{
|
||||||
title: '오늘 매출',
|
title: '오늘의 매출',
|
||||||
value: '₩567,000',
|
value: 123000,
|
||||||
change: '+12%',
|
displayValue: '₩123,000',
|
||||||
|
change: '+15%',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-cash',
|
icon: 'mdi-cash-multiple',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
|
detail: '목표 달성률: 85%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '월 매출',
|
title: '이번 달 매출',
|
||||||
value: '₩12,340,000',
|
value: 2450000,
|
||||||
|
displayValue: '₩2,450,000',
|
||||||
change: '+8%',
|
change: '+8%',
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-chart-line',
|
icon: 'mdi-trending-up',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
|
detail: '목표까지 ₩450,000',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '콘텐츠 수',
|
title: '일일 조회수',
|
||||||
value: '24',
|
value: 1234,
|
||||||
change: '+3',
|
displayValue: '1,234',
|
||||||
trend: 'up',
|
change: '+23%',
|
||||||
icon: 'mdi-file-document',
|
|
||||||
color: 'info',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '조회수',
|
|
||||||
value: '15.2K',
|
|
||||||
change: '+25%',
|
|
||||||
trend: 'up',
|
trend: 'up',
|
||||||
icon: 'mdi-eye',
|
icon: 'mdi-eye',
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
|
detail: '참여율: 4.2%',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 빠른 액션
|
// AI 추천 (간소화)
|
||||||
const quickActions = ref([
|
|
||||||
{
|
|
||||||
title: 'SNS 콘텐츠',
|
|
||||||
icon: 'mdi-plus-circle',
|
|
||||||
color: 'primary',
|
|
||||||
action: () => router.push('/content/create?type=sns'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '포스터 생성',
|
|
||||||
icon: 'mdi-image-plus',
|
|
||||||
color: 'secondary',
|
|
||||||
action: () => router.push('/content/create?type=poster'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '메뉴 등록',
|
|
||||||
icon: 'mdi-food-apple',
|
|
||||||
color: 'success',
|
|
||||||
action: () => router.push('/menu'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '매출 분석',
|
|
||||||
icon: 'mdi-chart-bar',
|
|
||||||
color: 'info',
|
|
||||||
action: () => router.push('/sales'),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// AI 추천
|
|
||||||
const aiRecommendations = ref([
|
const aiRecommendations = ref([
|
||||||
{
|
{
|
||||||
type: 'info',
|
type: 'info',
|
||||||
icon: 'mdi-weather-rainy',
|
icon: 'mdi-weather-rainy',
|
||||||
title: '날씨 기반 추천',
|
title: '날씨 기반 추천',
|
||||||
content: '오늘은 비가 와서 따뜻한 음식이 인기 있을 것 같아요. 국물 요리를 추천해보세요!',
|
content: '오늘은 비가 와서 따뜻한 음식이 인기 있을 것 같아요.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -259,83 +277,372 @@ const aiRecommendations = ref([
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
icon: 'mdi-clock-outline',
|
icon: 'mdi-clock-outline',
|
||||||
title: '시간대 팁',
|
title: '시간대 팁',
|
||||||
content: '점심시간(12-14시)에 주문이 집중됩니다. 미리 준비하세요.',
|
content: '점심시간(12-14시)에 주문이 집중됩니다.',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 최근 활동
|
// 가짜 차트 데이터 (Chart.js 없을 때 표시용)
|
||||||
const recentActivities = ref([
|
const fakeChartData = ref([65, 78, 82, 45, 90, 67, 88])
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'SNS 콘텐츠 "떡볶이 신메뉴 출시" 발행',
|
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 30),
|
|
||||||
icon: 'mdi-instagram',
|
|
||||||
color: 'purple',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '메뉴 "치즈떡볶이" 등록',
|
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
|
||||||
icon: 'mdi-food',
|
|
||||||
color: 'orange',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '매장 정보 업데이트',
|
|
||||||
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
|
||||||
icon: 'mdi-store',
|
|
||||||
color: 'blue',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 메서드
|
// 숫자 애니메이션 함수
|
||||||
const refreshAIRecommendations = async () => {
|
const animateNumber = (targetValue, index, duration = 2000) => {
|
||||||
|
const startValue = 0
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const updateValue = () => {
|
||||||
|
const elapsed = Date.now() - startTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
|
||||||
|
// easeOutCubic 애니메이션
|
||||||
|
const easedProgress = 1 - Math.pow(1 - progress, 3)
|
||||||
|
const currentValue = Math.floor(startValue + (targetValue - startValue) * easedProgress)
|
||||||
|
|
||||||
|
// 숫자 형식에 따라 표시
|
||||||
|
if (typeof targetValue === 'number') {
|
||||||
|
if (index === 0 || index === 1) {
|
||||||
|
// 매출 - 원화 표시
|
||||||
|
animatedValues.value[index] = `₩${currentValue.toLocaleString()}`
|
||||||
|
} else {
|
||||||
|
// 일반 숫자 (조회수 등)
|
||||||
|
animatedValues.value[index] = currentValue.toLocaleString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(updateValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 지표 애니메이션 시작
|
||||||
|
const startMetricsAnimation = () => {
|
||||||
|
dashboardMetrics.value.forEach((metric, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
animateNumber(metric.value, index, 1500 + index * 200)
|
||||||
|
}, index * 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 차트 업데이트
|
||||||
|
const updateChart = (period) => {
|
||||||
|
console.log('차트 기간 변경:', period)
|
||||||
|
// Chart.js 구현 시 여기에 차트 업데이트 로직 추가
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart.js 초기화 (설치 후 사용)
|
||||||
|
/*
|
||||||
|
const initSalesChart = () => {
|
||||||
|
const ctx = document.getElementById('salesChart')
|
||||||
|
if (ctx && window.Chart) {
|
||||||
|
chartInitialized.value = true
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['6일전', '5일전', '4일전', '3일전', '2일전', '어제', '오늘'],
|
||||||
|
datasets: [{
|
||||||
|
label: '매출액',
|
||||||
|
data: [390000, 425000, 520000, 380000, 450000, 520000, 567000],
|
||||||
|
borderColor: '#1976D2',
|
||||||
|
backgroundColor: 'rgba(25, 118, 210, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
pointBackgroundColor: '#1976D2',
|
||||||
|
pointBorderColor: '#ffffff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleColor: '#ffffff',
|
||||||
|
bodyColor: '#ffffff',
|
||||||
|
borderColor: '#1976D2',
|
||||||
|
borderWidth: 1,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return '매출: ₩' + context.parsed.y.toLocaleString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#666666'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: '#666666',
|
||||||
|
callback: function(value) {
|
||||||
|
return '₩' + (value / 1000) + 'K'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 데이터 새로고침
|
||||||
|
const refreshData = async () => {
|
||||||
try {
|
try {
|
||||||
aiLoading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// API 호출 시뮬레이션
|
// API 호출 시뮬레이션
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
appStore.showSnackbar('AI 추천이 갱신되었습니다', 'success')
|
|
||||||
|
console.log('대시보드 데이터 새로고침 완료')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI 추천 갱신 실패:', error)
|
console.error('데이터 새로고침 실패:', error)
|
||||||
appStore.showSnackbar('AI 추천 갱신에 실패했습니다', 'error')
|
appStore.showSnackbar('데이터를 불러오는데 실패했습니다.', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
aiLoading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이프사이클
|
// 라이프사이클
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('DashboardView 마운트됨')
|
console.log('DashboardView 마운트됨')
|
||||||
console.log('사용자 정보:', authStore.user)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
// 초기 데이터 로드
|
||||||
|
await refreshData()
|
||||||
|
|
||||||
// 대시보드 데이터 로드 시뮬레이션
|
// 지표 애니메이션 시작
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
setTimeout(() => {
|
||||||
|
startMetricsAnimation()
|
||||||
|
}, 500)
|
||||||
|
|
||||||
console.log('대시보드 데이터 로드 완료')
|
// Chart.js 초기화 (설치 후 주석 해제)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// initSalesChart()
|
||||||
|
// }, 1000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('대시보드 로드 실패:', error)
|
console.error('대시보드 초기화 실패:', error)
|
||||||
appStore.showSnackbar('대시보드를 불러오는데 실패했습니다', 'error')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 애니메이션 정리
|
||||||
|
animatedValues.value = {}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bg-gradient-primary {
|
/* 메트릭 카드 스타일 */
|
||||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
.metric-card {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-100 {
|
.metric-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, var(--v-theme-primary), var(--v-theme-primary-lighten-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card--success::before {
|
||||||
|
background: linear-gradient(90deg, var(--v-theme-success), var(--v-theme-success-lighten-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card--warning::before {
|
||||||
|
background: linear-gradient(90deg, var(--v-theme-warning), var(--v-theme-warning-lighten-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon-wrapper {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-trend {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-detail {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 차트 카드 스타일 */
|
||||||
|
.chart-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sales-chart-container {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sales-chart {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px dashed #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 가짜 차트 스타일 */
|
||||||
|
.fake-chart {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-chart-bars {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 8px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-bar {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
.fake-bar-fill {
|
||||||
.text-h5 {
|
width: 100%;
|
||||||
font-size: 1.3rem !important;
|
background: linear-gradient(to top, #1976d2, #42a5f5);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: barGrow 1s ease-out forwards;
|
||||||
|
transform-origin: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-bar-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes barGrow {
|
||||||
|
from {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleY(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI 추천 카드 스타일 */
|
||||||
|
.ai-recommend-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-recommend-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-recommendation-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-recommendation-item:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 디자인 */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.metric-value {
|
||||||
|
font-size: 1.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon-wrapper {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.metric-value {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-chart-bars {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 다크 테마 지원 */
|
||||||
|
.v-theme--dark .metric-card {
|
||||||
|
background-color: rgb(var(--v-theme-surface-variant));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-theme--dark .chart-placeholder {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-theme--dark .fake-bar-fill {
|
||||||
|
background: linear-gradient(to top, #60a5fa, #93c5fd);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user