From 985f780ab9c7f435bb57cf146c9eb18031204266 Mon Sep 17 00:00:00 2001 From: SeoJHeasdw Date: Fri, 13 Jun 2025 14:37:39 +0900 Subject: [PATCH] release --- src/views/DashboardView.vue | 532 +++++++++++++++++++++++++++--------- 1 file changed, 397 insertions(+), 135 deletions(-) diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 863a9e9..594b0f5 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -220,42 +220,178 @@ - + - -
- mdi-robot -

AI 추천 활용

+ +
+
+ mdi-robot +
+

AI 추천 활용

+

맞춤형 마케팅 제안

+
+
+
-

맞춤형 마케팅 제안

- + +

AI가 분석 중...

-
-
- - - {{ recommendation.title }} - -
- {{ recommendation.content }} -
-
+ +
+ +
+
+ {{ aiRecommendation.emoji }} +

+ {{ aiRecommendation.title }} +

+
+ + +
+ +
+
+ + {{ aiRecommendation.sections.ideas.title }} +
+
+
+
+ + +
+
+
+
+ + +
+
+ + {{ aiRecommendation.sections.costs.title }} +
+ + +
+ + + + {{ cost.item }} + {{ cost.amount }} + + + +
+ + +
+

📈 기대 효과:

+
+
+ + {{ effect }} +
+
+
+
+ + +
+
+ + {{ aiRecommendation.sections.warnings.title }} +
+
+
+
+ + +
+
+
+
+ + +
+
+ + {{ aiRecommendation.currentInfo.title }} +
+
+
+ + {{ info.label }}: {{ info.value }} + +
+
+
+ + +
+
+
+
+
+ + +
+ + +
+ +

{{ aiError }}

+ + 다시 시도 + +
+ + +
+ +

+ AI 추천을 불러오고 있습니다 +

+ + 추천 받기 +
@@ -287,9 +423,8 @@ import { formatCurrency, formatNumber, formatRelativeTime } from '@/utils/format /** * 대시보드 메인 페이지 - App.vue의 단일 AppBar 사용 - * - 중복된 AppBar 제거 (App.vue에서 제공) - * - 로그아웃 확인 다이얼로그 유지 - * - 메인 컨텐츠는 그대로 유지 + * - AI 추천을 단일 상세 콘텐츠로 변경 + * - Claude API 연동 준비된 구조 */ const router = useRouter() @@ -304,6 +439,7 @@ const animatedValues = ref({}) const logoutDialog = ref(false) const chartCanvas = ref(null) const currentTime = ref('') +const aiError = ref('') // 툴팁 관련 const tooltip = ref({ @@ -315,7 +451,7 @@ const tooltip = ref({ target: 0 }) -// 대시보드 지표 - 제목과 증감율 위치 변경 +// 대시보드 지표 const dashboardMetrics = ref([ { title: '오늘의 매출', @@ -346,7 +482,7 @@ const dashboardMetrics = ref([ }, ]) -// 실제 차트 데이터 (더 구체적이고 현실적인 데이터) +// 차트 데이터 const chartData = ref({ '7d': [ { label: '6일전', sales: 45, target: 50, date: '06-04' }, @@ -372,49 +508,69 @@ const chartData = ref({ ], }) -// Y축 라벨 const yAxisLabels = ref(['0', '25', '50', '75', '100']) -// AI 추천 -const aiRecommendations = ref([ - { - type: 'info', +// AI 추천 데이터 (Claude API 연동용 구조) +const aiRecommendation = ref({ + emoji: '☀️', + title: '여름 시즌 인스타그램 마케팅 계획', + sections: { + ideas: { + title: '1. 기획 아이디어', + items: [ + '여름 음료 메뉴 개발 예: 시원한 아이스 아메리카노, 프라페 등', + '카페 내부에서 음료와 함께 촬영한 인스타그램용 사진 및 영상 제작', + '지역 인플루언서와 협업하여 방문 후기 및 신메뉴 소개 게시물 게시', + '인스타그램 스토리를 활용해 매일 음료 프로모션 소식 공유' + ] + }, + costs: { + title: '2. 예상 비용 및 기대 효과', + items: [ + { item: '촬영 및 편집', amount: '약 300,000원' }, + { item: '인플루언서 협찬', amount: '약 200,000원' } + ], + effects: [ + '고객 관심 유도 및 매출 상승', + 'SNS를 통한 브랜드 인지도 상승', + '재방문율 및 공유 유도' + ] + }, + warnings: { + title: '3. 주의사항 및 유의점', + items: [ + '인스타그램 콘텐츠는 창의적이고 시각적으로 매력적이어야 함', + '인플루언서 협업 시, 합리적인 혜택과 협의 조건 필요' + ] + } + }, + currentInfo: { + title: '현재 지역 날씨 (서울 강남구 역삼동 기준)', icon: 'mdi-weather-sunny', - title: '날씨 기반 추천', - content: '오늘은 맑은 날씨로 야외 테이크아웃 주문이 증가할 예정입니다.', - }, - { - type: 'success', - icon: 'mdi-trending-up', - title: '트렌드 알림', - content: '최근 #떡볶이챌린지가 인기입니다. 관련 콘텐츠를 만들어보세요.', - }, - { - type: 'warning', - icon: 'mdi-clock-outline', - title: '시간대 팁', - content: '점심시간(12-14시) 주문 집중. 미리 재료를 준비하세요.', - }, -]) + color: 'orange', + items: [ + { label: '기온', value: '30도' }, + { label: '기상 상황', value: '무더위 지속' } + ], + insight: '시원한 음료에 대한 수요가 매우 높을 것으로 예상' + } +}) // 계산된 속성들 const currentChartData = computed(() => chartData.value[chartPeriod.value]) -// 수정된 chartDataPoints - 차트 컨테이너의 실제 여백을 고려 const chartDataPoints = computed(() => { const data = currentChartData.value const maxSales = Math.max(...data.map(d => Math.max(d.sales, d.target))) return data.map((item, index) => { - // 차트 영역의 실제 너비를 고려한 위치 계산 - // 차트 컨테이너에서 Y축 라벨 영역(60px)과 오른쪽 여백(20px)을 제외한 영역에서 계산 - const chartStartPercent = 8 // 차트 시작 지점 (약간의 여백) - const chartEndPercent = 92 // 차트 끝 지점 (약간의 여백) + const chartStartPercent = 8 + const chartEndPercent = 92 const chartWidth = chartEndPercent - chartStartPercent return { x: chartStartPercent + (index * chartWidth / (data.length - 1)), - y: 10 + ((item.sales / maxSales) * 80), // 10%에서 90%까지 높이 + y: 10 + ((item.sales / maxSales) * 80), targetY: 10 + ((item.target / maxSales) * 80), sales: item.sales, target: item.target, @@ -444,7 +600,7 @@ const achievementRate = computed(() => { return Math.round((totalSales / totalTarget) * 100) }) -// 현재 기간 라벨 +// 메서드들 const getCurrentPeriodLabel = () => { switch (chartPeriod.value) { case '7d': return '7일' @@ -454,21 +610,6 @@ const getCurrentPeriodLabel = () => { } } -// 시간 업데이트 -const updateTime = () => { - const now = new Date() - const options = { - year: 'numeric', - month: '2-digit', - day: '2-digit', - weekday: 'short', - hour: '2-digit', - minute: '2-digit' - } - currentTime.value = now.toLocaleString('ko-KR', options) -} - -// 숫자 애니메이션 함수 const animateNumber = (targetValue, index, duration = 2000) => { const startValue = 0 const startTime = Date.now() @@ -477,17 +618,13 @@ const animateNumber = (targetValue, index, duration = 2000) => { 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() } } @@ -500,7 +637,6 @@ const animateNumber = (targetValue, index, duration = 2000) => { updateValue() } -// 모든 지표 애니메이션 시작 const startMetricsAnimation = () => { dashboardMetrics.value.forEach((metric, index) => { setTimeout(() => { @@ -509,7 +645,6 @@ const startMetricsAnimation = () => { }) } -// 차트 그리기 const drawChart = async () => { await nextTick() @@ -519,7 +654,6 @@ const drawChart = async () => { const ctx = canvas.getContext('2d') const data = currentChartData.value - // 캔버스 초기화 ctx.clearRect(0, 0, canvas.width, canvas.height) const padding = 60 @@ -569,14 +703,12 @@ const drawChart = async () => { ctx.setLineDash([]) } -// 차트 업데이트 const updateChart = async (period) => { console.log('차트 기간 변경:', period) await nextTick() drawChart() } -// 툴팁 표시 const showDataTooltip = (index, event) => { const data = currentChartData.value[index] const unit = chartPeriod.value === '90d' ? 100 : chartPeriod.value === '30d' ? 10 : 1 @@ -603,14 +735,76 @@ const hideTooltip = () => { tooltip.value.show = false } -// 로그아웃 핸들러 (App.vue의 로그아웃 버튼과 연결 가능) -const handleLogout = () => { - logoutDialog.value = true +// AI 추천 관련 메서드들 +const refreshAiRecommendation = async () => { + console.log('AI 추천 새로고침') + aiLoading.value = true + aiError.value = '' + + try { + // Claude API 호출 시뮬레이션 + await new Promise(resolve => setTimeout(resolve, 2000)) + + // 실제 Claude API 호출은 여기서 + // const response = await callClaudeAPI(prompt) + // aiRecommendation.value = parseClaudeResponse(response) + + console.log('AI 추천 새로고침 완료') + appStore.showSnackbar('AI 추천이 업데이트되었습니다', 'success') + } catch (error) { + console.error('AI 추천 로드 실패:', error) + aiError.value = 'AI 추천을 불러오는데 실패했습니다' + appStore.showSnackbar('AI 추천 로드에 실패했습니다', 'error') + } finally { + aiLoading.value = false + } +} + +const copyRecommendation = async () => { + try { + let text = `${aiRecommendation.value.emoji} ${aiRecommendation.value.title}\n\n` + + // 각 섹션을 텍스트로 변환 + Object.values(aiRecommendation.value.sections).forEach(section => { + text += `${section.title}\n` + if (section.items) { + section.items.forEach(item => { + // HTML 태그 제거 + const cleanItem = item.replace(/<[^>]*>/g, '') + text += `• ${cleanItem}\n` + }) + } + if (section.effects) { + text += '\n기대 효과:\n' + section.effects.forEach(effect => { + text += `• ${effect}\n` + }) + } + text += '\n' + }) + + await navigator.clipboard.writeText(text) + appStore.showSnackbar('추천 내용이 복사되었습니다', 'success') + } catch (error) { + console.error('복사 실패:', error) + appStore.showSnackbar('복사에 실패했습니다', 'error') + } +} + +const createContentFromRecommendation = () => { + // 추천 내용을 기반으로 콘텐츠 생성 페이지로 이동 + router.push({ + path: '/content/create', + query: { + type: 'sns', + title: aiRecommendation.value.title, + template: 'ai_recommendation' + } + }) } const confirmLogout = () => { try { - // 로그아웃 처리 authStore.logout() appStore.showSnackbar('로그아웃되었습니다.', 'success') router.push('/login') @@ -622,40 +816,11 @@ const confirmLogout = () => { } } -// 데이터 새로고침 -const refreshData = async () => { - try { - loading.value = true - - // API 호출 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 1000)) - - console.log('대시보드 데이터 새로고침 완료') - } catch (error) { - console.error('데이터 새로고침 실패:', error) - appStore.showSnackbar('데이터를 불러오는데 실패했습니다.', 'error') - } finally { - loading.value = false - } -} - -// 로그아웃 함수를 전역에서 호출할 수 있도록 노출 (App.vue에서 사용 가능) -defineExpose({ - handleLogout -}) - // 라이프사이클 onMounted(async () => { console.log('DashboardView 마운트됨') try { - // 시간 업데이트 - updateTime() - setInterval(updateTime, 60000) // 1분마다 업데이트 - - // 초기 데이터 로드 - await refreshData() - // 지표 애니메이션 시작 setTimeout(() => { startMetricsAnimation() @@ -665,19 +830,24 @@ onMounted(async () => { setTimeout(() => { drawChart() }, 1000) + + // AI 추천 로드 + setTimeout(() => { + // 자동으로 AI 추천을 로드하지 않고 사용자 액션 대기 + // refreshAiRecommendation() + }, 1500) } catch (error) { console.error('대시보드 초기화 실패:', error) } }) onBeforeUnmount(() => { - // 애니메이션 정리 animatedValues.value = {} }) \ No newline at end of file