release
This commit is contained in:
parent
d2356fb723
commit
bfc4d600e2
@ -1,157 +1,172 @@
|
|||||||
//* src/services/content.js - 기존 파일 수정 (API 설계서 기준)
|
//* src/services/content.js
|
||||||
import { contentApi, handleApiError, formatSuccessResponse } from './api.js'
|
import { contentApi, handleApiError, formatSuccessResponse } from './api.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마케팅 콘텐츠 관련 API 서비스
|
* 마케팅 콘텐츠 관련 API 서비스
|
||||||
* API 설계서 기준으로 수정됨
|
* 백엔드 SnsContentCreateRequest DTO에 맞게 수정
|
||||||
*/
|
*/
|
||||||
class ContentService {
|
class ContentService {
|
||||||
/**
|
/**
|
||||||
* SNS 게시물 생성 (CON-005: SNS 게시물 생성)
|
* SNS 게시물 생성
|
||||||
* @param {Object} contentData - SNS 콘텐츠 생성 정보
|
* @param {Object} contentData - SNS 콘텐츠 생성 정보
|
||||||
* @returns {Promise<Object>} 생성된 SNS 콘텐츠
|
* @returns {Promise<Object>} 생성된 SNS 콘텐츠
|
||||||
*/
|
*/
|
||||||
async generateSnsContent(contentData) {
|
async generateSnsContent(contentData) {
|
||||||
try {
|
try {
|
||||||
const response = await contentApi.post('/sns/generate', {
|
console.log('🚀 SNS 콘텐츠 생성 요청:', contentData)
|
||||||
storeId: contentData.storeId,
|
|
||||||
platform: 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 || '보통'
|
|
||||||
})
|
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, 'SNS 게시물이 생성되었습니다.')
|
// 백엔드 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 || 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
|
console.error('❌ SNS 콘텐츠 생성 실패:', error)
|
||||||
return handleApiError(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 콘텐츠 정보
|
* @param {Object} saveData - 저장할 SNS 콘텐츠 정보
|
||||||
* @returns {Promise<Object>} 저장 결과
|
* @returns {Promise<Object>} 저장 결과
|
||||||
*/
|
*/
|
||||||
async saveSnsContent(saveData) {
|
async saveSnsContent(saveData) {
|
||||||
try {
|
try {
|
||||||
const response = await contentApi.post('/sns/save', {
|
console.log('💾 SNS 콘텐츠 저장 요청:', saveData)
|
||||||
|
|
||||||
|
// 백엔드 SnsContentSaveRequest DTO에 맞는 구조로 변환
|
||||||
|
const requestData = {
|
||||||
title: saveData.title,
|
title: saveData.title,
|
||||||
content: saveData.content,
|
content: saveData.content,
|
||||||
hashtags: saveData.hashtags,
|
hashtags: saveData.hashtags || [],
|
||||||
platform: saveData.platform,
|
platform: this.mapPlatform(saveData.platform),
|
||||||
category: saveData.category,
|
category: saveData.category || '메뉴소개',
|
||||||
toneAndManner: saveData.toneAndManner,
|
// 백엔드 DTO에서 지원하는 필드들만 포함
|
||||||
targetAudience: saveData.targetAudience,
|
|
||||||
promotionalType: saveData.promotionalType,
|
|
||||||
eventName: saveData.eventName,
|
eventName: saveData.eventName,
|
||||||
eventDate: saveData.eventDate,
|
eventDate: saveData.eventDate,
|
||||||
status: saveData.status || 'DRAFT'
|
status: saveData.status || 'DRAFT'
|
||||||
})
|
}
|
||||||
|
|
||||||
|
console.log('📤 저장 요청 데이터:', requestData)
|
||||||
|
|
||||||
|
const response = await contentApi.post('/sns/save', requestData)
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.')
|
return formatSuccessResponse(response.data.data, 'SNS 게시물이 저장되었습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ SNS 콘텐츠 저장 실패:', error)
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 홍보 포스터 생성 (CON-015: 홍보 포스터 생성)
|
* 콘텐츠 목록 조회
|
||||||
* @param {Object} posterData - 포스터 생성 정보
|
* @param {Object} filters - 필터 조건
|
||||||
* @returns {Promise<Object>} 생성된 포스터
|
|
||||||
*/
|
|
||||||
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<Object>} 저장 결과
|
|
||||||
*/
|
|
||||||
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 - 필터링 옵션
|
|
||||||
* @returns {Promise<Object>} 콘텐츠 목록
|
* @returns {Promise<Object>} 콘텐츠 목록
|
||||||
*/
|
*/
|
||||||
async getContents(filters = {}) {
|
async getContentList(filters = {}) {
|
||||||
try {
|
try {
|
||||||
const queryParams = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
if (filters.contentType) queryParams.append('contentType', filters.contentType)
|
if (filters.contentType) params.append('contentType', filters.contentType)
|
||||||
if (filters.platform) queryParams.append('platform', filters.platform)
|
if (filters.platform) params.append('platform', filters.platform)
|
||||||
if (filters.period) queryParams.append('period', filters.period)
|
if (filters.period) params.append('period', filters.period)
|
||||||
if (filters.sortBy) queryParams.append('sortBy', filters.sortBy || 'latest')
|
if (filters.sortBy) params.append('sortBy', filters.sortBy)
|
||||||
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 response = await contentApi.get(`/list?${params.toString()}`)
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '콘텐츠 목록을 조회했습니다.')
|
return formatSuccessResponse(response.data.data, '콘텐츠 목록을 조회했습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleApiError(error)
|
console.error('❌ 콘텐츠 목록 조회 실패:', error)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 진행 중인 콘텐츠 조회
|
|
||||||
* @param {string} period - 조회 기간
|
|
||||||
* @returns {Promise<Object>} 진행 중인 콘텐츠 목록
|
|
||||||
*/
|
|
||||||
async getOngoingContents(period = 'month') {
|
|
||||||
try {
|
|
||||||
const response = await contentApi.get(`/ongoing?period=${period}`)
|
|
||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '진행 중인 콘텐츠를 조회했습니다.')
|
|
||||||
} catch (error) {
|
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,48 +182,26 @@ class ContentService {
|
|||||||
|
|
||||||
return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.')
|
return formatSuccessResponse(response.data.data, '콘텐츠 상세 정보를 조회했습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ 콘텐츠 상세 조회 실패:', error)
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 콘텐츠 수정
|
* 콘텐츠 삭제
|
||||||
* @param {number} contentId - 콘텐츠 ID
|
|
||||||
* @param {Object} updateData - 수정할 콘텐츠 정보
|
|
||||||
* @returns {Promise<Object>} 수정 결과
|
|
||||||
*/
|
|
||||||
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
|
* @param {number} contentId - 콘텐츠 ID
|
||||||
* @returns {Promise<Object>} 삭제 결과
|
* @returns {Promise<Object>} 삭제 결과
|
||||||
*/
|
*/
|
||||||
async deleteContent(contentId) {
|
async deleteContent(contentId) {
|
||||||
try {
|
try {
|
||||||
await contentApi.delete(`/${contentId}`)
|
const response = await contentApi.delete(`/${contentId}`)
|
||||||
|
|
||||||
return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.')
|
return formatSuccessResponse(null, '콘텐츠가 삭제되었습니다.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ 콘텐츠 삭제 실패:', error)
|
||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const contentService = new ContentService()
|
export default new ContentService()
|
||||||
export default contentService
|
|
||||||
@ -1,168 +1,307 @@
|
|||||||
//* src/store/content.js 수정 - 기존 구조 유지하고 API 연동만 추가
|
//* src/store/content.js
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import ContentService from '@/services/content.js'
|
||||||
import contentService from '@/services/content'
|
import { PLATFORM_SPECS, PLATFORM_LABELS } from '@/utils/constants'
|
||||||
|
|
||||||
export const useContentStore = defineStore('content', () => {
|
/**
|
||||||
// 기존 상태들 유지
|
* 콘텐츠 관련 상태 관리
|
||||||
const contentList = ref([])
|
*/
|
||||||
const ongoingContents = ref([])
|
export const useContentStore = defineStore('content', {
|
||||||
const selectedContent = ref(null)
|
state: () => ({
|
||||||
const generatedContent = ref(null)
|
// 콘텐츠 목록
|
||||||
const isLoading = ref(false)
|
contentList: [],
|
||||||
|
totalCount: 0,
|
||||||
|
|
||||||
// 기존 computed 속성들 유지
|
// 선택된 콘텐츠
|
||||||
const contentCount = computed(() => contentList.value.length)
|
selectedContent: null,
|
||||||
const ongoingContentCount = computed(() => ongoingContents.value.length)
|
|
||||||
|
|
||||||
// generateContent를 실제 API 호출로 수정
|
// 로딩 상태
|
||||||
const generateContent = async (type, formData) => {
|
loading: false,
|
||||||
isLoading.value = true
|
generating: false,
|
||||||
|
|
||||||
try {
|
// 필터 상태
|
||||||
let result
|
filters: {
|
||||||
if (type === 'sns') {
|
contentType: '',
|
||||||
result = await contentService.generateSnsContent(formData)
|
platform: '',
|
||||||
} else if (type === 'poster') {
|
period: '',
|
||||||
result = await contentService.generatePoster(formData)
|
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) {
|
if (state.filters.platform) {
|
||||||
generatedContent.value = result.data
|
filtered = filtered.filter(content => content.platform === state.filters.platform)
|
||||||
return { success: true, data: result.data }
|
}
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
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 {
|
} else {
|
||||||
return { success: false, error: result.message }
|
return aValue > bValue ? 1 : -1
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
|
||||||
} finally {
|
return filtered
|
||||||
isLoading.value = false
|
},
|
||||||
}
|
|
||||||
}
|
/**
|
||||||
|
* 페이지네이션된 콘텐츠 목록
|
||||||
// saveContent를 실제 API 호출로 수정
|
*/
|
||||||
const saveContent = async (type, contentData) => {
|
paginatedContents: (state) => {
|
||||||
isLoading.value = true
|
const start = (state.pagination.page - 1) * state.pagination.itemsPerPage
|
||||||
|
const end = start + state.pagination.itemsPerPage
|
||||||
try {
|
return state.filteredContents.slice(start, end)
|
||||||
let result
|
},
|
||||||
if (type === 'sns') {
|
|
||||||
result = await contentService.saveSnsContent(contentData)
|
/**
|
||||||
} else if (type === 'poster') {
|
* 총 페이지 수
|
||||||
result = await contentService.savePoster(contentData)
|
*/
|
||||||
}
|
totalPages: (state) => {
|
||||||
|
return Math.ceil(state.filteredContents.length / state.pagination.itemsPerPage)
|
||||||
if (result.success) {
|
}
|
||||||
// 콘텐츠 목록 새로고침
|
},
|
||||||
await fetchContentList()
|
|
||||||
return { success: true, message: '콘텐츠가 저장되었습니다.' }
|
actions: {
|
||||||
} else {
|
/**
|
||||||
return { success: false, error: result.message }
|
* 콘텐츠 생성 (AI 기반)
|
||||||
}
|
*/
|
||||||
} catch (error) {
|
async generateContent(contentData) {
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
this.generating = true
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
try {
|
||||||
}
|
console.log('🎯 콘텐츠 생성 시작:', contentData)
|
||||||
}
|
|
||||||
|
// 백엔드 DTO에 맞는 데이터 구조로 변환
|
||||||
// fetchContentList를 실제 API 호출로 수정
|
const requestData = {
|
||||||
const fetchContentList = async (filters = {}) => {
|
storeId: 1, // 현재는 하드코딩, 추후 로그인한 사용자의 매장 ID 사용
|
||||||
isLoading.value = true
|
storeName: '테스트 매장', // 추후 실제 매장 정보 사용
|
||||||
|
storeType: '음식점', // 추후 실제 매장 업종 사용
|
||||||
try {
|
platform: contentData.platform,
|
||||||
const result = await contentService.getContents(filters)
|
title: contentData.title,
|
||||||
|
category: contentData.category || this.mapTargetToCategory(contentData.targetType),
|
||||||
if (result.success) {
|
requirement: contentData.requirements || '',
|
||||||
contentList.value = result.data
|
target: contentData.targetType || '일반 고객',
|
||||||
return { success: true }
|
contentType: 'SNS 게시물',
|
||||||
} else {
|
eventName: contentData.eventName,
|
||||||
return { success: false, error: result.message }
|
startDate: contentData.startDate,
|
||||||
}
|
endDate: contentData.endDate,
|
||||||
} catch (error) {
|
images: contentData.images || [],
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
photoStyle: '밝고 화사한',
|
||||||
} finally {
|
includeHashtags: true,
|
||||||
isLoading.value = false
|
includeEmojis: true,
|
||||||
}
|
includeCallToAction: true,
|
||||||
}
|
includeLocationInfo: false
|
||||||
|
}
|
||||||
// fetchOngoingContents를 실제 API 호출로 수정
|
|
||||||
const fetchOngoingContents = async (period = 'month') => {
|
const result = await ContentService.generateSnsContent(requestData)
|
||||||
isLoading.value = true
|
|
||||||
|
if (result.success) {
|
||||||
try {
|
console.log('✅ 콘텐츠 생성 성공:', result.data)
|
||||||
const result = await contentService.getOngoingContents(period)
|
return result.data
|
||||||
|
} else {
|
||||||
if (result.success) {
|
throw new Error(result.message || '콘텐츠 생성에 실패했습니다.')
|
||||||
ongoingContents.value = result.data
|
}
|
||||||
return { success: true }
|
} catch (error) {
|
||||||
} else {
|
console.error('❌ 콘텐츠 생성 실패:', error)
|
||||||
return { success: false, error: result.message }
|
throw error
|
||||||
}
|
} finally {
|
||||||
} catch (error) {
|
this.generating = false
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
}
|
||||||
} finally {
|
},
|
||||||
isLoading.value = false
|
|
||||||
}
|
/**
|
||||||
}
|
* 타겟 타입을 카테고리로 매핑
|
||||||
|
*/
|
||||||
// 콘텐츠 수정/삭제 메서드 추가
|
mapTargetToCategory(targetType) {
|
||||||
const updateContent = async (contentId, updateData) => {
|
const mapping = {
|
||||||
isLoading.value = true
|
'new_menu': '메뉴소개',
|
||||||
|
'discount': '이벤트',
|
||||||
try {
|
'store': '인테리어',
|
||||||
const result = await contentService.updateContent(contentId, updateData)
|
'event': '이벤트'
|
||||||
|
}
|
||||||
if (result.success) {
|
return mapping[targetType] || '메뉴소개'
|
||||||
await fetchContentList()
|
},
|
||||||
return { success: true, message: '콘텐츠가 수정되었습니다.' }
|
|
||||||
} else {
|
/**
|
||||||
return { success: false, error: result.message }
|
* 플랫폼별 특성 조회
|
||||||
}
|
*/
|
||||||
} catch (error) {
|
getPlatformSpec(platform) {
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
return PLATFORM_SPECS[platform] || null
|
||||||
} finally {
|
},
|
||||||
isLoading.value = false
|
|
||||||
}
|
/**
|
||||||
}
|
* 플랫폼 유효성 검사
|
||||||
|
*/
|
||||||
const deleteContent = async (contentId) => {
|
validatePlatform(platform) {
|
||||||
isLoading.value = true
|
return Object.keys(PLATFORM_SPECS).includes(platform)
|
||||||
|
},
|
||||||
try {
|
|
||||||
const result = await contentService.deleteContent(contentId)
|
/**
|
||||||
|
* 콘텐츠 저장
|
||||||
if (result.success) {
|
*/
|
||||||
await fetchContentList()
|
async saveContent(contentData) {
|
||||||
return { success: true, message: '콘텐츠가 삭제되었습니다.' }
|
this.loading = true
|
||||||
} else {
|
|
||||||
return { success: false, error: result.message }
|
try {
|
||||||
}
|
// 백엔드 DTO에 맞는 형식으로 데이터 정제
|
||||||
} catch (error) {
|
const saveData = {
|
||||||
return { success: false, error: '네트워크 오류가 발생했습니다.' }
|
title: contentData.title,
|
||||||
} finally {
|
content: contentData.content,
|
||||||
isLoading.value = false
|
hashtags: contentData.hashtags || [],
|
||||||
}
|
platform: contentData.platform, // 이미 백엔드 형식 (INSTAGRAM, NAVER_BLOG 등)
|
||||||
}
|
category: contentData.category || '메뉴소개',
|
||||||
|
eventName: contentData.eventName,
|
||||||
return {
|
eventDate: contentData.eventDate,
|
||||||
// 상태
|
status: contentData.status || 'DRAFT'
|
||||||
contentList,
|
}
|
||||||
ongoingContents,
|
|
||||||
selectedContent,
|
const result = await ContentService.saveSnsContent(saveData)
|
||||||
generatedContent,
|
|
||||||
isLoading,
|
if (result.success) {
|
||||||
|
// 목록 새로고침
|
||||||
// 컴퓨티드
|
await this.fetchContentList()
|
||||||
contentCount,
|
return result.data
|
||||||
ongoingContentCount,
|
} else {
|
||||||
|
throw new Error(result.message || '콘텐츠 저장에 실패했습니다.')
|
||||||
// 메서드
|
}
|
||||||
generateContent,
|
} catch (error) {
|
||||||
saveContent,
|
console.error('❌ 콘텐츠 저장 실패:', error)
|
||||||
fetchContentList,
|
throw error
|
||||||
fetchOngoingContents,
|
} finally {
|
||||||
updateContent,
|
this.loading = false
|
||||||
deleteContent
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 목록 조회
|
||||||
|
*/
|
||||||
|
async fetchContentList() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콘텐츠 상세 조회
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -7,15 +7,7 @@
|
|||||||
export const CONTENT_TYPES = {
|
export const CONTENT_TYPES = {
|
||||||
SNS: 'sns',
|
SNS: 'sns',
|
||||||
POSTER: 'poster',
|
POSTER: 'poster',
|
||||||
VIDEO: 'video',
|
BLOG: 'blog'
|
||||||
BLOG: 'blog',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CONTENT_TYPE_LABELS = {
|
|
||||||
[CONTENT_TYPES.SNS]: 'SNS 게시물',
|
|
||||||
[CONTENT_TYPES.POSTER]: '홍보 포스터',
|
|
||||||
[CONTENT_TYPES.VIDEO]: '비디오',
|
|
||||||
[CONTENT_TYPES.BLOG]: '블로그 포스트',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 플랫폼
|
// 플랫폼
|
||||||
@ -23,191 +15,201 @@ export const PLATFORMS = {
|
|||||||
INSTAGRAM: 'instagram',
|
INSTAGRAM: 'instagram',
|
||||||
NAVER_BLOG: 'naver_blog',
|
NAVER_BLOG: 'naver_blog',
|
||||||
FACEBOOK: 'facebook',
|
FACEBOOK: 'facebook',
|
||||||
TWITTER: 'twitter',
|
KAKAO_STORY: 'kakao_story'
|
||||||
YOUTUBE: 'youtube',
|
|
||||||
KAKAO: 'kakao',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 플랫폼 라벨
|
||||||
export const PLATFORM_LABELS = {
|
export const PLATFORM_LABELS = {
|
||||||
[PLATFORMS.INSTAGRAM]: '인스타그램',
|
[PLATFORMS.INSTAGRAM]: '인스타그램',
|
||||||
[PLATFORMS.NAVER_BLOG]: '네이버 블로그',
|
[PLATFORMS.NAVER_BLOG]: '네이버 블로그',
|
||||||
[PLATFORMS.FACEBOOK]: '페이스북',
|
[PLATFORMS.FACEBOOK]: '페이스북',
|
||||||
[PLATFORMS.TWITTER]: '트위터',
|
[PLATFORMS.KAKAO_STORY]: '카카오스토리'
|
||||||
[PLATFORMS.YOUTUBE]: '유튜브',
|
|
||||||
[PLATFORMS.KAKAO]: '카카오',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 플랫폼 컬러
|
||||||
export const PLATFORM_COLORS = {
|
export const PLATFORM_COLORS = {
|
||||||
[PLATFORMS.INSTAGRAM]: 'purple',
|
[PLATFORMS.INSTAGRAM]: 'pink',
|
||||||
[PLATFORMS.NAVER_BLOG]: 'green',
|
[PLATFORMS.NAVER_BLOG]: 'green',
|
||||||
[PLATFORMS.FACEBOOK]: 'blue',
|
[PLATFORMS.FACEBOOK]: 'blue',
|
||||||
[PLATFORMS.TWITTER]: 'light-blue',
|
[PLATFORMS.KAKAO_STORY]: 'amber'
|
||||||
[PLATFORMS.YOUTUBE]: 'red',
|
|
||||||
[PLATFORMS.KAKAO]: 'yellow',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 콘텐츠 상태
|
// 플랫폼 아이콘
|
||||||
export const CONTENT_STATUS = {
|
export const PLATFORM_ICONS = {
|
||||||
DRAFT: 'draft',
|
[PLATFORMS.INSTAGRAM]: 'mdi-instagram',
|
||||||
PUBLISHED: 'published',
|
[PLATFORMS.NAVER_BLOG]: 'mdi-web',
|
||||||
SCHEDULED: 'scheduled',
|
[PLATFORMS.FACEBOOK]: 'mdi-facebook',
|
||||||
ARCHIVED: 'archived',
|
[PLATFORMS.KAKAO_STORY]: 'mdi-chat'
|
||||||
FAILED: 'failed',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CONTENT_STATUS_LABELS = {
|
// 플랫폼 사양 정의 (누락된 PLATFORM_SPECS 추가)
|
||||||
[CONTENT_STATUS.DRAFT]: '임시저장',
|
export const PLATFORM_SPECS = {
|
||||||
[CONTENT_STATUS.PUBLISHED]: '발행됨',
|
[PLATFORMS.INSTAGRAM]: {
|
||||||
[CONTENT_STATUS.SCHEDULED]: '예약됨',
|
name: '인스타그램',
|
||||||
[CONTENT_STATUS.ARCHIVED]: '보관됨',
|
icon: 'mdi-instagram',
|
||||||
[CONTENT_STATUS.FAILED]: '실패',
|
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 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]: '기타',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 톤앤매너
|
// 톤앤매너
|
||||||
export const TONE_AND_MANNER = {
|
export const TONE_AND_MANNER = {
|
||||||
FRIENDLY: 'friendly',
|
FRIENDLY: 'friendly',
|
||||||
PROFESSIONAL: 'professional',
|
PROFESSIONAL: 'professional',
|
||||||
HUMOROUS: 'humorous',
|
|
||||||
ELEGANT: 'elegant',
|
|
||||||
CASUAL: 'casual',
|
CASUAL: 'casual',
|
||||||
TRENDY: 'trendy',
|
HUMOROUS: 'humorous'
|
||||||
}
|
|
||||||
|
|
||||||
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]: '트렌디',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 감정 강도
|
// 감정 강도
|
||||||
export const EMOTION_INTENSITY = {
|
export const EMOTION_INTENSITY = {
|
||||||
CALM: 'calm',
|
LOW: 'low',
|
||||||
NORMAL: 'normal',
|
MEDIUM: 'medium',
|
||||||
ENTHUSIASTIC: 'enthusiastic',
|
HIGH: 'high'
|
||||||
EXCITING: 'exciting',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EMOTION_INTENSITY_LABELS = {
|
|
||||||
[EMOTION_INTENSITY.CALM]: '차분함',
|
|
||||||
[EMOTION_INTENSITY.NORMAL]: '보통',
|
|
||||||
[EMOTION_INTENSITY.ENTHUSIASTIC]: '열정적',
|
|
||||||
[EMOTION_INTENSITY.EXCITING]: '과장된',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 프로모션 타입
|
// 프로모션 타입
|
||||||
export const PROMOTION_TYPES = {
|
export const PROMOTION_TYPES = {
|
||||||
DISCOUNT: 'discount',
|
DISCOUNT: 'DISCOUNT',
|
||||||
EVENT: 'event',
|
EVENT: 'EVENT',
|
||||||
NEW_MENU: 'new_menu',
|
NEW_PRODUCT: 'NEW_PRODUCT',
|
||||||
NONE: 'none',
|
REVIEW: 'REVIEW'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PROMOTION_TYPE_LABELS = {
|
// 사진 스타일
|
||||||
[PROMOTION_TYPES.DISCOUNT]: '할인 정보',
|
|
||||||
[PROMOTION_TYPES.EVENT]: '이벤트 정보',
|
|
||||||
[PROMOTION_TYPES.NEW_MENU]: '신메뉴 알림',
|
|
||||||
[PROMOTION_TYPES.NONE]: '없음',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미지 스타일
|
|
||||||
export const PHOTO_STYLES = {
|
export const PHOTO_STYLES = {
|
||||||
MODERN: 'modern',
|
MODERN: 'modern',
|
||||||
CLASSIC: 'classic',
|
VINTAGE: 'vintage',
|
||||||
EMOTIONAL: 'emotional',
|
|
||||||
MINIMALIST: 'minimalist',
|
MINIMALIST: 'minimalist',
|
||||||
|
COLORFUL: 'colorful',
|
||||||
|
BRIGHT: 'bright',
|
||||||
|
CALM: 'calm',
|
||||||
|
NATURAL: 'natural'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PHOTO_STYLE_LABELS = {
|
// 콘텐츠 상태
|
||||||
[PHOTO_STYLES.MODERN]: '모던',
|
export const CONTENT_STATUS = {
|
||||||
[PHOTO_STYLES.CLASSIC]: '클래식',
|
DRAFT: 'draft',
|
||||||
[PHOTO_STYLES.EMOTIONAL]: '감성적',
|
PUBLISHED: 'published',
|
||||||
[PHOTO_STYLES.MINIMALIST]: '미니멀',
|
ARCHIVED: 'archived'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 업로드 제한
|
// 타겟 대상
|
||||||
export const FILE_LIMITS = {
|
export const TARGET_TYPES = {
|
||||||
MAX_SIZE: 10 * 1024 * 1024, // 10MB
|
NEW_MENU: 'new_menu',
|
||||||
ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
DISCOUNT: 'discount',
|
||||||
ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
|
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 응답 상태
|
// API 응답 상태
|
||||||
export const API_STATUS = {
|
export const API_STATUS = {
|
||||||
SUCCESS: 'success',
|
SUCCESS: 'success',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
LOADING: 'loading',
|
LOADING: 'loading'
|
||||||
IDLE: 'idle',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이지네이션
|
// 페이지 크기
|
||||||
export const PAGINATION = {
|
export const PAGE_SIZES = {
|
||||||
DEFAULT_PAGE_SIZE: 20,
|
SMALL: 10,
|
||||||
PAGE_SIZE_OPTIONS: [10, 20, 50, 100],
|
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 = {
|
export const STORAGE_KEYS = {
|
||||||
AUTH_TOKEN: 'auth_token',
|
ACCESS_TOKEN: 'accessToken',
|
||||||
USER_INFO: 'user_info',
|
REFRESH_TOKEN: 'refreshToken',
|
||||||
APP_SETTINGS: 'app_settings',
|
USER_INFO: 'userInfo',
|
||||||
CONTENT_FILTERS: 'content_filters',
|
THEME: 'theme',
|
||||||
}
|
LANGUAGE: 'language'
|
||||||
|
|
||||||
// 시간 관련 상수
|
|
||||||
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]: '전체',
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -122,7 +122,7 @@
|
|||||||
<div class="y-axis-labels">
|
<div class="y-axis-labels">
|
||||||
<div v-for="(label, i) in yAxisLabels" :key="i"
|
<div v-for="(label, i) in yAxisLabels" :key="i"
|
||||||
class="y-label"
|
class="y-label"
|
||||||
:style="{ bottom: `${i * 20}%` }">
|
:style="{ bottom: `${i * 18}%` }">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -139,7 +139,7 @@
|
|||||||
ref="chartCanvas"
|
ref="chartCanvas"
|
||||||
class="chart-canvas"
|
class="chart-canvas"
|
||||||
width="800"
|
width="800"
|
||||||
height="300"
|
height="600"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@mouseleave="hideTooltip">
|
@mouseleave="hideTooltip">
|
||||||
</canvas>
|
</canvas>
|
||||||
@ -176,7 +176,7 @@
|
|||||||
|
|
||||||
<!-- X축 라벨 - 데이터 포인트와 동일한 위치에 배치 -->
|
<!-- X축 라벨 - 데이터 포인트와 동일한 위치에 배치 -->
|
||||||
<div class="x-axis-labels mt-3" style="position: relative; height: 20px;">
|
<div class="x-axis-labels mt-3" style="position: relative; height: 20px;">
|
||||||
<div class="x-axis-container" style="position: relative; padding-left: 60px; padding-right: 20px;">
|
<div class="x-axis-container" style="position: relative; padding-left: 60px; padding-right: 60px;">
|
||||||
<span
|
<span
|
||||||
v-for="(point, index) in chartDataPoints"
|
v-for="(point, index) in chartDataPoints"
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -975,13 +975,8 @@ const updateAiRecommendation = (aiData) => {
|
|||||||
title: aiData.tipContent ? aiData.tipContent.substring(0, 50) + '...' : 'AI 마케팅 추천',
|
title: aiData.tipContent ? aiData.tipContent.substring(0, 50) + '...' : 'AI 마케팅 추천',
|
||||||
sections: {
|
sections: {
|
||||||
ideas: {
|
ideas: {
|
||||||
title: '1. 추천 아이디어',
|
title: '추천 아이디어',
|
||||||
items: [aiData.tipContent || '맞춤형 마케팅 전략을 제안드립니다.']
|
items: [aiData.tipContent || '맞춤형 마케팅 전략을 제안드립니다.']
|
||||||
},
|
|
||||||
costs: {
|
|
||||||
title: '2. 예상 효과',
|
|
||||||
items: ['고객 관심 유도 및 매출 상승', 'SNS를 통한 브랜드 인지도 상승'],
|
|
||||||
effects: ['재방문율 및 공유 유도', '지역 내 인지도 향상']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1025,17 +1020,30 @@ const chartDataPoints = computed(() => {
|
|||||||
const data = currentChartData.value
|
const data = currentChartData.value
|
||||||
if (!data || data.length === 0) return []
|
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) => {
|
return data.map((item, index) => {
|
||||||
const chartStartPercent = 8
|
// Canvas에서 그려지는 실제 좌표 계산
|
||||||
const chartEndPercent = 92
|
const canvasX = padding + (index * chartWidth / (data.length - 1))
|
||||||
const chartWidth = chartEndPercent - chartStartPercent
|
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 {
|
return {
|
||||||
x: chartStartPercent + (index * chartWidth / (data.length - 1)),
|
x: xPercent,
|
||||||
y: 10 + ((item.sales / maxSales) * 80),
|
y: yPercent,
|
||||||
targetY: 10 + ((item.target / maxSales) * 80),
|
targetY: targetYPercent,
|
||||||
sales: item.sales,
|
sales: item.sales,
|
||||||
target: item.target,
|
target: item.target,
|
||||||
label: item.label
|
label: item.label
|
||||||
@ -1479,7 +1487,7 @@ onMounted(async () => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 40px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.y-label {
|
.y-label {
|
||||||
@ -1491,8 +1499,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.chart-grid {
|
.chart-grid {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 60px;
|
||||||
right: 0;
|
right: 60px;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
@ -1515,7 +1523,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.data-points {
|
.data-points {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40px;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user