From bfc4d600e21944629a3f5ddd77c4e0592685b3d3 Mon Sep 17 00:00:00 2001 From: SeoJHeasdw Date: Tue, 17 Jun 2025 17:44:14 +0900 Subject: [PATCH] release --- src/services/content.js | 273 +++--- src/store/content.js | 435 ++++++--- src/utils/constants.js | 294 +++--- src/views/ContentCreationView.vue | 1502 +++++++++++++++-------------- src/views/DashboardView.vue | 48 +- 5 files changed, 1399 insertions(+), 1153 deletions(-) diff --git a/src/services/content.js b/src/services/content.js index 1a3a44f..99ee797 100644 --- a/src/services/content.js +++ b/src/services/content.js @@ -1,157 +1,172 @@ -//* src/services/content.js - 기존 파일 수정 (API 설계서 기준) +//* src/services/content.js import { contentApi, handleApiError, formatSuccessResponse } from './api.js' /** * 마케팅 콘텐츠 관련 API 서비스 - * API 설계서 기준으로 수정됨 + * 백엔드 SnsContentCreateRequest DTO에 맞게 수정 */ class ContentService { /** - * SNS 게시물 생성 (CON-005: SNS 게시물 생성) + * SNS 게시물 생성 * @param {Object} contentData - SNS 콘텐츠 생성 정보 * @returns {Promise} 생성된 SNS 콘텐츠 */ async generateSnsContent(contentData) { try { - const response = await contentApi.post('/sns/generate', { - storeId: contentData.storeId, - platform: contentData.platform, + console.log('🚀 SNS 콘텐츠 생성 요청:', contentData) + + // 백엔드 SnsContentCreateRequest DTO에 맞는 데이터 구조 + const requestData = { + // === 기본 정보 === + storeId: contentData.storeId || 1, + storeName: contentData.storeName || '테스트 매장', + storeType: contentData.storeType || '음식점', + platform: this.mapPlatform(contentData.platform), title: contentData.title, - category: contentData.category, - requirement: contentData.requirement || contentData.requirements, - toneAndManner: contentData.toneAndManner, - emotionalIntensity: contentData.emotionalIntensity || contentData.emotionIntensity, - targetAudience: contentData.targetAudience, - promotionalType: contentData.promotionalType || contentData.promotionType, - eventName: contentData.eventName, - eventDate: contentData.eventDate, - hashtagStyle: contentData.hashtagStyle, - hashtagCount: contentData.hashtagCount || 10, - includeCallToAction: contentData.includeCallToAction || false, - includeEmoji: contentData.includeEmoji || true, - contentLength: contentData.contentLength || '보통' - }) + + // === 콘텐츠 생성 조건 === + category: contentData.category || this.mapTargetToCategory(contentData.targetType), + requirement: contentData.requirements || contentData.content || '', + target: contentData.targetType || '일반 고객', + contentType: 'SNS 게시물', + + // === 이벤트 정보 === + eventName: contentData.eventName || null, + startDate: contentData.startDate ? this.formatDate(contentData.startDate) : null, + endDate: contentData.endDate ? this.formatDate(contentData.endDate) : null, + + // === 미디어 정보 === + images: contentData.images || [], + photoStyle: this.mapPhotoStyle(contentData.aiOptions?.photoStyle), + + // === 추가 옵션 === + includeHashtags: true, + includeEmojis: true, + includeCallToAction: true, + includeLocationInfo: false + } - return formatSuccessResponse(response.data.data, 'SNS 게시물이 생성되었습니다.') + console.log('📤 백엔드 DTO 맞춤 데이터:', requestData) + + const response = await contentApi.post('/sns/generate', requestData) + + console.log('📥 API 응답:', response.data) + + // 응답 데이터 구조에 맞게 처리 + const responseData = response.data.data || response.data + + return formatSuccessResponse({ + content: responseData.content || responseData, + hashtags: responseData.hashtags || [], + ...responseData + }, 'SNS 게시물이 생성되었습니다.') } catch (error) { + console.error('❌ SNS 콘텐츠 생성 실패:', error) return handleApiError(error) } } /** - * SNS 게시물 저장 (CON-010: SNS 게시물 저장) + * 플랫폼 매핑 (프론트엔드 -> 백엔드) + */ + mapPlatform(platform) { + const mapping = { + 'instagram': 'INSTAGRAM', + 'naver_blog': 'NAVER_BLOG', + 'facebook': 'FACEBOOK', + 'kakao_story': 'KAKAO_STORY' + } + return mapping[platform] || 'INSTAGRAM' + } + + /** + * 타겟 타입을 카테고리로 매핑 + */ + mapTargetToCategory(targetType) { + const mapping = { + 'new_menu': '메뉴소개', + 'discount': '이벤트', + 'store': '인테리어', + 'event': '이벤트' + } + return mapping[targetType] || '메뉴소개' + } + + /** + * 날짜 형식 변환 (YYYY-MM-DD -> LocalDate) + */ + formatDate(dateString) { + if (!dateString) return null + // YYYY-MM-DD 형식이 LocalDate와 호환됨 + return dateString + } + + /** + * 사진 스타일 매핑 + */ + mapPhotoStyle(style) { + const mapping = { + 'bright': '밝고 화사한', + 'calm': '차분하고 세련된', + 'vintage': '빈티지한', + 'modern': '모던한', + 'natural': '자연스러운' + } + return mapping[style] || '밝고 화사한' + } + + /** + * SNS 게시물 저장 * @param {Object} saveData - 저장할 SNS 콘텐츠 정보 * @returns {Promise} 저장 결과 */ async saveSnsContent(saveData) { try { - const response = await contentApi.post('/sns/save', { + console.log('💾 SNS 콘텐츠 저장 요청:', saveData) + + // 백엔드 SnsContentSaveRequest DTO에 맞는 구조로 변환 + const requestData = { title: saveData.title, content: saveData.content, - hashtags: saveData.hashtags, - platform: saveData.platform, - category: saveData.category, - toneAndManner: saveData.toneAndManner, - targetAudience: saveData.targetAudience, - promotionalType: saveData.promotionalType, + hashtags: saveData.hashtags || [], + platform: this.mapPlatform(saveData.platform), + category: saveData.category || '메뉴소개', + // 백엔드 DTO에서 지원하는 필드들만 포함 eventName: saveData.eventName, eventDate: saveData.eventDate, status: saveData.status || 'DRAFT' - }) + } + + console.log('📤 저장 요청 데이터:', requestData) + + const response = await contentApi.post('/sns/save', requestData) return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.') } catch (error) { + console.error('❌ SNS 콘텐츠 저장 실패:', error) return handleApiError(error) } } /** - * 홍보 포스터 생성 (CON-015: 홍보 포스터 생성) - * @param {Object} posterData - 포스터 생성 정보 - * @returns {Promise} 생성된 포스터 - */ - async generatePoster(posterData) { - try { - const response = await contentApi.post('/poster/generate', { - storeId: posterData.storeId, - title: posterData.title, - targetType: posterData.targetType, - eventName: posterData.eventName, - eventDate: posterData.eventDate, - discountInfo: posterData.discountInfo, - designStyle: posterData.designStyle, - colorScheme: posterData.colorScheme, - includeQrCode: posterData.includeQrCode || false, - includeContact: posterData.includeContact || true, - imageStyle: posterData.imageStyle || posterData.photoStyle, - layoutType: posterData.layoutType, - sizes: posterData.sizes || ['1:1', '9:16', '16:9'] - }) - - return formatSuccessResponse(response.data.data, '홍보 포스터가 생성되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 홍보 포스터 저장 (CON-016: 홍보 포스터 저장) - * @param {Object} saveData - 저장할 포스터 정보 - * @returns {Promise} 저장 결과 - */ - async savePoster(saveData) { - try { - const response = await contentApi.post('/poster/save', { - title: saveData.title, - images: saveData.images, - posterSizes: saveData.posterSizes, - targetType: saveData.targetType, - eventName: saveData.eventName, - status: saveData.status || 'DRAFT' - }) - - return formatSuccessResponse(response.data.data, '홍보 포스터가 저장되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 콘텐츠 목록 조회 (CON-020: 마케팅 콘텐츠 이력) - * @param {Object} filters - 필터링 옵션 + * 콘텐츠 목록 조회 + * @param {Object} filters - 필터 조건 * @returns {Promise} 콘텐츠 목록 */ - async getContents(filters = {}) { + async getContentList(filters = {}) { try { - const queryParams = new URLSearchParams() - - if (filters.contentType) queryParams.append('contentType', filters.contentType) - if (filters.platform) queryParams.append('platform', filters.platform) - if (filters.period) queryParams.append('period', filters.period) - if (filters.sortBy) queryParams.append('sortBy', filters.sortBy || 'latest') - if (filters.page) queryParams.append('page', filters.page) - if (filters.size) queryParams.append('size', filters.size || 20) - if (filters.search) queryParams.append('search', filters.search) - - const response = await contentApi.get(`/?${queryParams.toString()}`) - + const params = new URLSearchParams() + + if (filters.contentType) params.append('contentType', filters.contentType) + if (filters.platform) params.append('platform', filters.platform) + if (filters.period) params.append('period', filters.period) + if (filters.sortBy) params.append('sortBy', filters.sortBy) + + const response = await contentApi.get(`/list?${params.toString()}`) + return formatSuccessResponse(response.data.data, '콘텐츠 목록을 조회했습니다.') } catch (error) { - return handleApiError(error) - } - } - - /** - * 진행 중인 콘텐츠 조회 - * @param {string} period - 조회 기간 - * @returns {Promise} 진행 중인 콘텐츠 목록 - */ - async getOngoingContents(period = 'month') { - try { - const response = await contentApi.get(`/ongoing?period=${period}`) - - return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.') - } catch (error) { + console.error('❌ 콘텐츠 목록 조회 실패:', error) return handleApiError(error) } } @@ -164,51 +179,29 @@ class ContentService { async getContentDetail(contentId) { try { const response = await contentApi.get(`/${contentId}`) - + return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.') } catch (error) { + console.error('❌ 콘텐츠 상세 조회 실패:', error) return handleApiError(error) } } /** - * 콘텐츠 수정 - * @param {number} contentId - 콘텐츠 ID - * @param {Object} updateData - 수정할 콘텐츠 정보 - * @returns {Promise} 수정 결과 - */ - async updateContent(contentId, updateData) { - try { - const response = await contentApi.put(`/${contentId}`, { - title: updateData.title, - content: updateData.content, - hashtags: updateData.hashtags, - startDate: updateData.startDate, - endDate: updateData.endDate, - status: updateData.status - }) - - return formatSuccessResponse(response.data.data, '콘텐츠가 수정되었습니다.') - } catch (error) { - return handleApiError(error) - } - } - - /** - * 콘텐츠 삭제 (CON-025: 콘텐츠 삭제) + * 콘텐츠 삭제 * @param {number} contentId - 콘텐츠 ID * @returns {Promise} 삭제 결과 */ async deleteContent(contentId) { try { - await contentApi.delete(`/${contentId}`) - + const response = await contentApi.delete(`/${contentId}`) + return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.') } catch (error) { + console.error('❌ 콘텐츠 삭제 실패:', error) return handleApiError(error) } } } -export const contentService = new ContentService() -export default contentService \ No newline at end of file +export default new ContentService() \ No newline at end of file diff --git a/src/store/content.js b/src/store/content.js index a1430e9..176e852 100644 --- a/src/store/content.js +++ b/src/store/content.js @@ -1,168 +1,307 @@ -//* src/store/content.js 수정 - 기존 구조 유지하고 API 연동만 추가 +//* src/store/content.js import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import contentService from '@/services/content' +import ContentService from '@/services/content.js' +import { PLATFORM_SPECS, PLATFORM_LABELS } from '@/utils/constants' -export const useContentStore = defineStore('content', () => { - // 기존 상태들 유지 - const contentList = ref([]) - const ongoingContents = ref([]) - const selectedContent = ref(null) - const generatedContent = ref(null) - const isLoading = ref(false) - - // 기존 computed 속성들 유지 - const contentCount = computed(() => contentList.value.length) - const ongoingContentCount = computed(() => ongoingContents.value.length) - - // generateContent를 실제 API 호출로 수정 - const generateContent = async (type, formData) => { - isLoading.value = true +/** + * 콘텐츠 관련 상태 관리 + */ +export const useContentStore = defineStore('content', { + state: () => ({ + // 콘텐츠 목록 + contentList: [], + totalCount: 0, - try { - let result - if (type === 'sns') { - result = await contentService.generateSnsContent(formData) - } else if (type === 'poster') { - result = await contentService.generatePoster(formData) + // 선택된 콘텐츠 + selectedContent: null, + + // 로딩 상태 + loading: false, + generating: false, + + // 필터 상태 + filters: { + contentType: '', + platform: '', + period: '', + sortBy: 'createdAt_desc' + }, + + // 페이지네이션 + pagination: { + page: 1, + itemsPerPage: 10 + } + }), + + getters: { + /** + * 필터링된 콘텐츠 목록 + */ + filteredContents: (state) => { + let filtered = [...state.contentList] + + if (state.filters.contentType) { + filtered = filtered.filter(content => content.type === state.filters.contentType) } - if (result.success) { - generatedContent.value = result.data - return { success: true, data: result.data } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // saveContent를 실제 API 호출로 수정 - const saveContent = async (type, contentData) => { - isLoading.value = true - - try { - let result - if (type === 'sns') { - result = await contentService.saveSnsContent(contentData) - } else if (type === 'poster') { - result = await contentService.savePoster(contentData) + if (state.filters.platform) { + filtered = filtered.filter(content => content.platform === state.filters.platform) } - if (result.success) { - // 콘텐츠 목록 새로고침 - await fetchContentList() - return { success: true, message: '콘텐츠가 저장되었습니다.' } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } - - // fetchContentList를 실제 API 호출로 수정 - const fetchContentList = async (filters = {}) => { - isLoading.value = true - - try { - const result = await contentService.getContents(filters) + // 정렬 + const [field, order] = state.filters.sortBy.split('_') + filtered.sort((a, b) => { + let aValue = a[field] + let bValue = b[field] + + if (field === 'createdAt' || field === 'updatedAt') { + aValue = new Date(aValue) + bValue = new Date(bValue) + } + + if (order === 'desc') { + return bValue > aValue ? 1 : -1 + } else { + return aValue > bValue ? 1 : -1 + } + }) - if (result.success) { - contentList.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } - } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } + return filtered + }, - // fetchOngoingContents를 실제 API 호출로 수정 - const fetchOngoingContents = async (period = 'month') => { - isLoading.value = true - - try { - const result = await contentService.getOngoingContents(period) + /** + * 페이지네이션된 콘텐츠 목록 + */ + paginatedContents: (state) => { + const start = (state.pagination.page - 1) * state.pagination.itemsPerPage + const end = start + state.pagination.itemsPerPage + return state.filteredContents.slice(start, end) + }, + + /** + * 총 페이지 수 + */ + totalPages: (state) => { + return Math.ceil(state.filteredContents.length / state.pagination.itemsPerPage) + } + }, + + actions: { + /** + * 콘텐츠 생성 (AI 기반) + */ + async generateContent(contentData) { + this.generating = true - if (result.success) { - ongoingContents.value = result.data - return { success: true } - } else { - return { success: false, error: result.message } + try { + console.log('🎯 콘텐츠 생성 시작:', contentData) + + // 백엔드 DTO에 맞는 데이터 구조로 변환 + const requestData = { + storeId: 1, // 현재는 하드코딩, 추후 로그인한 사용자의 매장 ID 사용 + storeName: '테스트 매장', // 추후 실제 매장 정보 사용 + storeType: '음식점', // 추후 실제 매장 업종 사용 + platform: contentData.platform, + title: contentData.title, + category: contentData.category || this.mapTargetToCategory(contentData.targetType), + requirement: contentData.requirements || '', + target: contentData.targetType || '일반 고객', + contentType: 'SNS 게시물', + eventName: contentData.eventName, + startDate: contentData.startDate, + endDate: contentData.endDate, + images: contentData.images || [], + photoStyle: '밝고 화사한', + includeHashtags: true, + includeEmojis: true, + includeCallToAction: true, + includeLocationInfo: false + } + + const result = await ContentService.generateSnsContent(requestData) + + if (result.success) { + console.log('✅ 콘텐츠 생성 성공:', result.data) + return result.data + } else { + throw new Error(result.message || '콘텐츠 생성에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 생성 실패:', error) + throw error + } finally { + this.generating = false } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } + }, - // 콘텐츠 수정/삭제 메서드 추가 - const updateContent = async (contentId, updateData) => { - isLoading.value = true - - try { - const result = await contentService.updateContent(contentId, updateData) + /** + * 타겟 타입을 카테고리로 매핑 + */ + mapTargetToCategory(targetType) { + const mapping = { + 'new_menu': '메뉴소개', + 'discount': '이벤트', + 'store': '인테리어', + 'event': '이벤트' + } + return mapping[targetType] || '메뉴소개' + }, + + /** + * 플랫폼별 특성 조회 + */ + getPlatformSpec(platform) { + return PLATFORM_SPECS[platform] || null + }, + + /** + * 플랫폼 유효성 검사 + */ + validatePlatform(platform) { + return Object.keys(PLATFORM_SPECS).includes(platform) + }, + + /** + * 콘텐츠 저장 + */ + async saveContent(contentData) { + this.loading = true - if (result.success) { - await fetchContentList() - return { success: true, message: '콘텐츠가 수정되었습니다.' } - } else { - return { success: false, error: result.message } + try { + // 백엔드 DTO에 맞는 형식으로 데이터 정제 + const saveData = { + title: contentData.title, + content: contentData.content, + hashtags: contentData.hashtags || [], + platform: contentData.platform, // 이미 백엔드 형식 (INSTAGRAM, NAVER_BLOG 등) + category: contentData.category || '메뉴소개', + eventName: contentData.eventName, + eventDate: contentData.eventDate, + status: contentData.status || 'DRAFT' + } + + const result = await ContentService.saveSnsContent(saveData) + + if (result.success) { + // 목록 새로고침 + await this.fetchContentList() + return result.data + } else { + throw new Error(result.message || '콘텐츠 저장에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 저장 실패:', error) + throw error + } finally { + this.loading = false } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } + }, - const deleteContent = async (contentId) => { - isLoading.value = true - - try { - const result = await contentService.deleteContent(contentId) + /** + * 콘텐츠 목록 조회 + */ + async fetchContentList() { + this.loading = true - if (result.success) { - await fetchContentList() - return { success: true, message: '콘텐츠가 삭제되었습니다.' } - } else { - return { success: false, error: result.message } + try { + const result = await ContentService.getContentList(this.filters) + + if (result.success) { + this.contentList = result.data || [] + this.totalCount = this.contentList.length + } else { + throw new Error(result.message || '콘텐츠 목록 조회에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 목록 조회 실패:', error) + this.contentList = [] + this.totalCount = 0 + } finally { + this.loading = false } - } catch (error) { - return { success: false, error: '네트워크 오류가 발생했습니다.' } - } finally { - isLoading.value = false - } - } + }, - return { - // 상태 - contentList, - ongoingContents, - selectedContent, - generatedContent, - isLoading, - - // 컴퓨티드 - contentCount, - ongoingContentCount, - - // 메서드 - generateContent, - saveContent, - fetchContentList, - fetchOngoingContents, - updateContent, - deleteContent + /** + * 콘텐츠 상세 조회 + */ + async fetchContentDetail(contentId) { + this.loading = true + + try { + const result = await ContentService.getContentDetail(contentId) + + if (result.success) { + this.selectedContent = result.data + return result.data + } else { + throw new Error(result.message || '콘텐츠 상세 조회에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 상세 조회 실패:', error) + throw error + } finally { + this.loading = false + } + }, + + /** + * 콘텐츠 삭제 + */ + async deleteContent(contentId) { + this.loading = true + + try { + const result = await ContentService.deleteContent(contentId) + + if (result.success) { + // 목록에서 제거 + this.contentList = this.contentList.filter(content => content.id !== contentId) + this.totalCount = this.contentList.length + return true + } else { + throw new Error(result.message || '콘텐츠 삭제에 실패했습니다.') + } + } catch (error) { + console.error('❌ 콘텐츠 삭제 실패:', error) + throw error + } finally { + this.loading = false + } + }, + + /** + * 필터 설정 + */ + setFilters(newFilters) { + this.filters = { ...this.filters, ...newFilters } + this.pagination.page = 1 // 필터 변경 시 첫 페이지로 + }, + + /** + * 페이지네이션 설정 + */ + setPagination(newPagination) { + this.pagination = { ...this.pagination, ...newPagination } + }, + + /** + * 상태 초기화 + */ + reset() { + this.contentList = [] + this.selectedContent = null + this.totalCount = 0 + this.filters = { + contentType: '', + platform: '', + period: '', + sortBy: 'createdAt_desc' + } + this.pagination = { + page: 1, + itemsPerPage: 10 + } + } } }) \ No newline at end of file diff --git a/src/utils/constants.js b/src/utils/constants.js index 26b368f..eb7213a 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -7,15 +7,7 @@ export const CONTENT_TYPES = { SNS: 'sns', POSTER: 'poster', - VIDEO: 'video', - BLOG: 'blog', -} - -export const CONTENT_TYPE_LABELS = { - [CONTENT_TYPES.SNS]: 'SNS 게시물', - [CONTENT_TYPES.POSTER]: '홍보 포스터', - [CONTENT_TYPES.VIDEO]: '비디오', - [CONTENT_TYPES.BLOG]: '블로그 포스트', + BLOG: 'blog' } // 플랫폼 @@ -23,191 +15,201 @@ export const PLATFORMS = { INSTAGRAM: 'instagram', NAVER_BLOG: 'naver_blog', FACEBOOK: 'facebook', - TWITTER: 'twitter', - YOUTUBE: 'youtube', - KAKAO: 'kakao', + KAKAO_STORY: 'kakao_story' } +// 플랫폼 라벨 export const PLATFORM_LABELS = { [PLATFORMS.INSTAGRAM]: '인스타그램', [PLATFORMS.NAVER_BLOG]: '네이버 블로그', [PLATFORMS.FACEBOOK]: '페이스북', - [PLATFORMS.TWITTER]: '트위터', - [PLATFORMS.YOUTUBE]: '유튜브', - [PLATFORMS.KAKAO]: '카카오', + [PLATFORMS.KAKAO_STORY]: '카카오스토리' } +// 플랫폼 컬러 export const PLATFORM_COLORS = { - [PLATFORMS.INSTAGRAM]: 'purple', + [PLATFORMS.INSTAGRAM]: 'pink', [PLATFORMS.NAVER_BLOG]: 'green', [PLATFORMS.FACEBOOK]: 'blue', - [PLATFORMS.TWITTER]: 'light-blue', - [PLATFORMS.YOUTUBE]: 'red', - [PLATFORMS.KAKAO]: 'yellow', + [PLATFORMS.KAKAO_STORY]: 'amber' } -// 콘텐츠 상태 -export const CONTENT_STATUS = { - DRAFT: 'draft', - PUBLISHED: 'published', - SCHEDULED: 'scheduled', - ARCHIVED: 'archived', - FAILED: 'failed', +// 플랫폼 아이콘 +export const PLATFORM_ICONS = { + [PLATFORMS.INSTAGRAM]: 'mdi-instagram', + [PLATFORMS.NAVER_BLOG]: 'mdi-web', + [PLATFORMS.FACEBOOK]: 'mdi-facebook', + [PLATFORMS.KAKAO_STORY]: 'mdi-chat' } -export const CONTENT_STATUS_LABELS = { - [CONTENT_STATUS.DRAFT]: '임시저장', - [CONTENT_STATUS.PUBLISHED]: '발행됨', - [CONTENT_STATUS.SCHEDULED]: '예약됨', - [CONTENT_STATUS.ARCHIVED]: '보관됨', - [CONTENT_STATUS.FAILED]: '실패', -} - -export const CONTENT_STATUS_COLORS = { - [CONTENT_STATUS.DRAFT]: 'orange', - [CONTENT_STATUS.PUBLISHED]: 'success', - [CONTENT_STATUS.SCHEDULED]: 'info', - [CONTENT_STATUS.ARCHIVED]: 'grey', - [CONTENT_STATUS.FAILED]: 'error', -} - -// 매장 업종 -export const BUSINESS_TYPES = { - RESTAURANT: 'restaurant', - CAFE: 'cafe', - SNACK_BAR: 'snack_bar', - FAST_FOOD: 'fast_food', - BAKERY: 'bakery', - DESSERT: 'dessert', - CONVENIENCE: 'convenience', - OTHER: 'other', -} - -export const BUSINESS_TYPE_LABELS = { - [BUSINESS_TYPES.RESTAURANT]: '일반음식점', - [BUSINESS_TYPES.CAFE]: '카페', - [BUSINESS_TYPES.SNACK_BAR]: '분식점', - [BUSINESS_TYPES.FAST_FOOD]: '패스트푸드', - [BUSINESS_TYPES.BAKERY]: '제과점', - [BUSINESS_TYPES.DESSERT]: '디저트카페', - [BUSINESS_TYPES.CONVENIENCE]: '편의점', - [BUSINESS_TYPES.OTHER]: '기타', +// 플랫폼 사양 정의 (누락된 PLATFORM_SPECS 추가) +export const PLATFORM_SPECS = { + [PLATFORMS.INSTAGRAM]: { + name: '인스타그램', + icon: 'mdi-instagram', + color: 'pink', + maxLength: 2200, + hashtags: true, + imageRequired: true, + format: 'sns' + }, + [PLATFORMS.NAVER_BLOG]: { + name: '네이버 블로그', + icon: 'mdi-web', + color: 'green', + maxLength: 5000, + hashtags: false, + imageRequired: false, + format: 'blog' + }, + [PLATFORMS.FACEBOOK]: { + name: '페이스북', + icon: 'mdi-facebook', + color: 'blue', + maxLength: 63206, + hashtags: true, + imageRequired: false, + format: 'sns' + }, + [PLATFORMS.KAKAO_STORY]: { + name: '카카오스토리', + icon: 'mdi-chat', + color: 'amber', + maxLength: 1000, + hashtags: true, + imageRequired: false, + format: 'sns' + } } // 톤앤매너 export const TONE_AND_MANNER = { FRIENDLY: 'friendly', PROFESSIONAL: 'professional', - HUMOROUS: 'humorous', - ELEGANT: 'elegant', CASUAL: 'casual', - TRENDY: 'trendy', -} - -export const TONE_AND_MANNER_LABELS = { - [TONE_AND_MANNER.FRIENDLY]: '친근함', - [TONE_AND_MANNER.PROFESSIONAL]: '전문적', - [TONE_AND_MANNER.HUMOROUS]: '유머러스', - [TONE_AND_MANNER.ELEGANT]: '고급스러운', - [TONE_AND_MANNER.CASUAL]: '캐주얼', - [TONE_AND_MANNER.TRENDY]: '트렌디', + HUMOROUS: 'humorous' } // 감정 강도 export const EMOTION_INTENSITY = { - CALM: 'calm', - NORMAL: 'normal', - ENTHUSIASTIC: 'enthusiastic', - EXCITING: 'exciting', -} - -export const EMOTION_INTENSITY_LABELS = { - [EMOTION_INTENSITY.CALM]: '차분함', - [EMOTION_INTENSITY.NORMAL]: '보통', - [EMOTION_INTENSITY.ENTHUSIASTIC]: '열정적', - [EMOTION_INTENSITY.EXCITING]: '과장된', + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high' } // 프로모션 타입 export const PROMOTION_TYPES = { - DISCOUNT: 'discount', - EVENT: 'event', - NEW_MENU: 'new_menu', - NONE: 'none', + DISCOUNT: 'DISCOUNT', + EVENT: 'EVENT', + NEW_PRODUCT: 'NEW_PRODUCT', + REVIEW: 'REVIEW' } -export const PROMOTION_TYPE_LABELS = { - [PROMOTION_TYPES.DISCOUNT]: '할인 정보', - [PROMOTION_TYPES.EVENT]: '이벤트 정보', - [PROMOTION_TYPES.NEW_MENU]: '신메뉴 알림', - [PROMOTION_TYPES.NONE]: '없음', -} - -// 이미지 스타일 +// 사진 스타일 export const PHOTO_STYLES = { MODERN: 'modern', - CLASSIC: 'classic', - EMOTIONAL: 'emotional', + VINTAGE: 'vintage', MINIMALIST: 'minimalist', + COLORFUL: 'colorful', + BRIGHT: 'bright', + CALM: 'calm', + NATURAL: 'natural' } -export const PHOTO_STYLE_LABELS = { - [PHOTO_STYLES.MODERN]: '모던', - [PHOTO_STYLES.CLASSIC]: '클래식', - [PHOTO_STYLES.EMOTIONAL]: '감성적', - [PHOTO_STYLES.MINIMALIST]: '미니멀', +// 콘텐츠 상태 +export const CONTENT_STATUS = { + DRAFT: 'draft', + PUBLISHED: 'published', + ARCHIVED: 'archived' } -// 파일 업로드 제한 -export const FILE_LIMITS = { - MAX_SIZE: 10 * 1024 * 1024, // 10MB - ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], - ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], +// 타겟 대상 +export const TARGET_TYPES = { + NEW_MENU: 'new_menu', + DISCOUNT: 'discount', + STORE: 'store', + EVENT: 'event' +} + +// 타겟 대상 라벨 +export const TARGET_TYPE_LABELS = { + [TARGET_TYPES.NEW_MENU]: '신메뉴', + [TARGET_TYPES.DISCOUNT]: '할인 이벤트', + [TARGET_TYPES.STORE]: '매장 홍보', + [TARGET_TYPES.EVENT]: '일반 이벤트' +} + +// 백엔드 플랫폼 매핑 (프론트엔드 -> 백엔드) +export const BACKEND_PLATFORM_MAPPING = { + [PLATFORMS.INSTAGRAM]: 'INSTAGRAM', + [PLATFORMS.NAVER_BLOG]: 'NAVER_BLOG', + [PLATFORMS.FACEBOOK]: 'FACEBOOK', + [PLATFORMS.KAKAO_STORY]: 'KAKAO_STORY' +} + +// 백엔드에서 프론트엔드로 매핑 (백엔드 -> 프론트엔드) +export const FRONTEND_PLATFORM_MAPPING = { + 'INSTAGRAM': PLATFORMS.INSTAGRAM, + 'NAVER_BLOG': PLATFORMS.NAVER_BLOG, + 'FACEBOOK': PLATFORMS.FACEBOOK, + 'KAKAO_STORY': PLATFORMS.KAKAO_STORY } // API 응답 상태 export const API_STATUS = { SUCCESS: 'success', ERROR: 'error', - LOADING: 'loading', - IDLE: 'idle', + LOADING: 'loading' } -// 페이지네이션 -export const PAGINATION = { - DEFAULT_PAGE_SIZE: 20, - PAGE_SIZE_OPTIONS: [10, 20, 50, 100], +// 페이지 크기 +export const PAGE_SIZES = { + SMALL: 10, + MEDIUM: 20, + LARGE: 50 +} + +// 정렬 방향 +export const SORT_DIRECTION = { + ASC: 'asc', + DESC: 'desc' +} + +// 날짜 포맷 +export const DATE_FORMATS = { + DISPLAY: 'YYYY-MM-DD HH:mm', + API: 'YYYY-MM-DD', + FULL: 'YYYY-MM-DD HH:mm:ss' +} + +// 파일 업로드 제한 +export const FILE_LIMITS = { + MAX_SIZE: 10485760, // 10MB + ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + MAX_FILES: 5 +} + +// 콘텐츠 생성 제한 +export const CONTENT_LIMITS = { + TITLE_MAX_LENGTH: 100, + DESCRIPTION_MAX_LENGTH: 500, + REQUIREMENTS_MAX_LENGTH: 1000, + MAX_HASHTAGS: 30 +} + +// 알림 타입 +export const NOTIFICATION_TYPES = { + SUCCESS: 'success', + ERROR: 'error', + WARNING: 'warning', + INFO: 'info' } // 로컬 스토리지 키 export const STORAGE_KEYS = { - AUTH_TOKEN: 'auth_token', - USER_INFO: 'user_info', - APP_SETTINGS: 'app_settings', - CONTENT_FILTERS: 'content_filters', -} - -// 시간 관련 상수 -export const TIME_FORMATS = { - DATE: 'YYYY-MM-DD', - DATETIME: 'YYYY-MM-DD HH:mm:ss', - TIME: 'HH:mm', -} - -export const DATE_RANGES = { - TODAY: 'today', - WEEK: 'week', - MONTH: 'month', - QUARTER: 'quarter', - YEAR: 'year', - ALL: 'all', -} - -export const DATE_RANGE_LABELS = { - [DATE_RANGES.TODAY]: '오늘', - [DATE_RANGES.WEEK]: '최근 1주일', - [DATE_RANGES.MONTH]: '최근 1개월', - [DATE_RANGES.QUARTER]: '최근 3개월', - [DATE_RANGES.YEAR]: '최근 1년', - [DATE_RANGES.ALL]: '전체', -} + ACCESS_TOKEN: 'accessToken', + REFRESH_TOKEN: 'refreshToken', + USER_INFO: 'userInfo', + THEME: 'theme', + LANGUAGE: 'language' +} \ No newline at end of file diff --git a/src/views/ContentCreationView.vue b/src/views/ContentCreationView.vue index be215ed..370b9d8 100644 --- a/src/views/ContentCreationView.vue +++ b/src/views/ContentCreationView.vue @@ -9,301 +9,251 @@ :class="['left-panel', { 'left-panel-full': generatedVersions.length === 0 }]" > - +
+ mdi-creation -

콘텐츠 생성 (최대 3개)

+

콘텐츠 생성

+ + + 생성 가능: {{ remainingGenerations }}회 +
- -
- - - 콘텐츠 유형 선택 - - - + + 콘텐츠 유형 선택 + + + + - - - {{ type.icon }} - -
- {{ type.label }} -
-
- {{ type.description }} -
-
-
-
+ {{ type.icon }} + +
+ {{ type.label }} +
+
+ {{ type.description }} +
+
+
+
+
+
+ + +
+ + + 기본 정보 + + + + + + + + + + + + + + + + + + + + + + + + + + - -
- - - 기본 정보 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 이미지 첨부 - - - - -
-
- - - mdi-close - -
-
-
-
- - - - - - mdi-arrow-right - 다음 - - - -
-
- - -
- - - mdi-arrow-left - 이전 - - - - - AI 옵션 설정 + + + + mdi-robot + AI 설정 + - + - - - - - - + - - - - + + + + mdi-camera + 사진 업로드 (선택사항) + - - + + + +
+ + + + + + + + +
+
+
- + + + - - {{ useAI ? 'mdi-robot' : 'mdi-content-save' }} - - {{ useAI ? `AI 콘텐츠 신규 생성 (${remainingGenerations}회)` : '콘텐츠 저장' }} + mdi-robot + AI로 콘텐츠 생성하기 - - -
- mdi-information - AI 생성은 최대 3회까지 가능합니다. (남은 횟수: {{ remainingGenerations }}회) + +
+ 생성 가능 횟수를 모두 사용했습니다.
@@ -312,140 +262,155 @@ - - + + - -
-
- mdi-file-document-multiple -

콘텐츠 생성 결과

-
+ +
+ mdi-eye +

생성된 콘텐츠

+ + + {{ generatedVersions.length }}개 생성됨 +
- -
- - mdi-file-document-outline - -

- 아직 생성된 콘텐츠가 없습니다 -

-

- 왼쪽에서 정보를 입력하고 '{{ useAI ? 'AI 콘텐츠 생성' : '콘텐츠 저장' }}' 버튼을 클릭해주세요 -

+ +
+

생성된 버전들

+ + + + +
+
+
+ 버전 {{ index + 1 }} +
+
+ {{ formatDateTime(version.createdAt) }} +
+
+
+ + {{ getStatusText(version.status) }} + + + mdi-chevron-right + +
+
+
+
+
+
- -
- - -
- - 버전 {{ index + 1 }} - - - {{ version.title }} - -
-
- - - -
- - {{ getPlatformIcon(version.platform) }} - {{ getPlatformLabel(version.platform) }} - - - {{ formatDate(version.createdAt) }} - -
- - -
-

- {{ truncateText(version.content, 120) }} -

-
- - -
- - #{{ tag }} - - - +{{ version.hashtags.length - 3 }}개 더 - -
- - -
- - - - - -

- +{{ version.images.length - 2 }}개 이미지 더 -

-
-
- - - + +
+

미리보기

+ + + + {{ getPlatformIcon(currentVersion.platform) }} + + {{ getPlatformLabel(currentVersion.platform) }} - mdi-send - 발행 + 자세히 보기 - + + + + + + +
+ {{ currentVersion.title }} +
+ + +
+
+ +
+
+ ... 더 보려면 '자세히 보기'를 클릭하세요 +
+
+
{{ truncateText(currentVersion.content, 150) }}
+
+ + + +
+ + {{ hashtag }} + + + +{{ currentVersion.hashtags.length - 5 }}개 더 + +
+ + +
+ + mdi-content-save + 저장하기 + + + + mdi-content-copy + 복사 + +
+
@@ -453,90 +418,125 @@ - + - - - {{ selectedVersionData.title }} (버전 {{ selectedVersion + 1 }}) + + + + {{ getPlatformIcon(currentVersion.platform) }} + + {{ currentVersion.title }} - - mdi-close - + - + + + - - +
- - {{ getPlatformIcon(selectedVersionData.platform) }} - {{ getPlatformLabel(selectedVersionData.platform) }} - - {{ formatDate(selectedVersionData.createdAt) }} -
- - -

콘텐츠

-
- {{ selectedVersionData.content }} +
+
+
+ {{ currentVersion.content }}
+ + -
+

해시태그

- - #{{ tag }} - -
- - -
-

이미지

- - + - - - + {{ hashtag }} + +
+
+ + +
+

정보

+ + + 플랫폼 + + + + 홍보 대상 + + + + 이벤트명 + + + + 생성일시 + + +
- + - - + + + - mdi-send - 발행하기 + mdi-content-copy + 전체 복사 + + + mdi-content-save + 저장하기 - +

AI가 콘텐츠를 생성 중입니다

@@ -547,20 +547,10 @@ \ No newline at end of file diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 2b3b52b..280e3ba 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -122,7 +122,7 @@
+ :style="{ bottom: `${i * 18}%` }"> {{ label }}
@@ -139,7 +139,7 @@ ref="chartCanvas" class="chart-canvas" width="800" - height="300" + height="600" @mousemove="handleMouseMove" @mouseleave="hideTooltip"> @@ -176,7 +176,7 @@
-
+
{ title: aiData.tipContent ? aiData.tipContent.substring(0, 50) + '...' : 'AI 마케팅 추천', sections: { ideas: { - title: '1. 추천 아이디어', + title: '추천 아이디어', items: [aiData.tipContent || '맞춤형 마케팅 전략을 제안드립니다.'] - }, - costs: { - title: '2. 예상 효과', - items: ['고객 관심 유도 및 매출 상승', 'SNS를 통한 브랜드 인지도 상승'], - effects: ['재방문율 및 공유 유도', '지역 내 인지도 향상'] } } } @@ -1025,17 +1020,30 @@ const chartDataPoints = computed(() => { const data = currentChartData.value if (!data || data.length === 0) return [] - const maxSales = Math.max(...data.map(d => Math.max(d.sales, d.target))) + const maxValue = Math.max(...data.map(d => Math.max(d.sales, d.target))) + + // Canvas의 실제 padding과 일치하는 좌표 계산 + const padding = 60 // drawChart에서 사용하는 padding과 동일 + const canvasWidth = 800 // Canvas width와 동일 + const canvasHeight = 300 // Canvas height와 동일 + const chartWidth = canvasWidth - padding * 2 + const chartHeight = canvasHeight - padding * 2 return data.map((item, index) => { - const chartStartPercent = 8 - const chartEndPercent = 92 - const chartWidth = chartEndPercent - chartStartPercent + // Canvas에서 그려지는 실제 좌표 계산 + const canvasX = padding + (index * chartWidth / (data.length - 1)) + const canvasY = padding + chartHeight - ((item.sales / maxValue) * chartHeight) + const targetCanvasY = padding + chartHeight - ((item.target / maxValue) * chartHeight) + + // 백분율로 변환하되, data-points의 padding을 고려 + const xPercent = (canvasX / canvasWidth) * 100 + const yPercent = ((canvasHeight - canvasY + padding) / canvasHeight) * 100 - 15 + const targetYPercent = ((canvasHeight - targetCanvasY + padding) / canvasHeight) * 100 - 15 return { - x: chartStartPercent + (index * chartWidth / (data.length - 1)), - y: 10 + ((item.sales / maxSales) * 80), - targetY: 10 + ((item.target / maxSales) * 80), + x: xPercent, + y: yPercent, + targetY: targetYPercent, sales: item.sales, target: item.target, label: item.label @@ -1479,7 +1487,7 @@ onMounted(async () => { left: 0; top: 0; bottom: 0; - width: 40px; + width: 50px; } .y-label { @@ -1491,8 +1499,8 @@ onMounted(async () => { .chart-grid { position: absolute; - left: 40px; - right: 0; + left: 60px; + right: 60px; top: 0; bottom: 0; } @@ -1515,7 +1523,7 @@ onMounted(async () => { .data-points { position: absolute; - left: 40px; + left: 0; right: 0; top: 0; bottom: 0;