426 lines
12 KiB
Vue
426 lines
12 KiB
Vue
<script setup>
|
|
import { ref, watch, computed } from 'vue'
|
|
import { X, FileText, Activity, Navigation, Search, CreditCard, Layout, Info, TrendingUp, Save } from 'lucide-vue-next'
|
|
import apiClient from '../api/client'
|
|
|
|
// 서브 컴포넌트 임포트
|
|
import BasicInfoReporting from './reports/BasicInfoReporting.vue'
|
|
import VisitorReporting from './reports/VisitorReporting.vue'
|
|
import InflowReporting from './reports/InflowReporting.vue'
|
|
import BlogReporting from './reports/BlogReporting.vue'
|
|
import ExposureReporting from './reports/ExposureReporting.vue'
|
|
import PlanReporting from './reports/PlanReporting.vue'
|
|
import SummaryReporting from './reports/SummaryReporting.vue'
|
|
import TransferReporting from './reports/TransferReporting.vue'
|
|
import DepositTrackingReporting from './reports/DepositTrackingReporting.vue'
|
|
|
|
const props = defineProps({
|
|
show: Boolean,
|
|
company: Object
|
|
})
|
|
|
|
const emit = defineEmits(['close', 'saved', 'deleted'])
|
|
|
|
const activeTab = ref('basic')
|
|
const formData = ref({
|
|
name: '',
|
|
companyInfo: { manager: '', contact: '', businessNumber: '', naverIdPw: '', blogLink: '', refLink: '' },
|
|
visitorData: { chart: [], table: [], startDate: '', endDate: '' },
|
|
viewData: { chart: [], table: [] },
|
|
inflowData: { chart: [], table: [], description: '' },
|
|
exposureStatus: [],
|
|
blogStatus: [],
|
|
depositDetails: { dates: '', table: [] },
|
|
transferInfo: { companyName: '', businessNumber: '', ceo: '', ourManager: '', bankName: '', account: '', holder: '', amount: '' },
|
|
comparisonData: { '방문자수': '', '조회수': '', '유입변화': '' },
|
|
diagnosisResult: '',
|
|
reportSummary: '',
|
|
nextMonthPlan: '',
|
|
blogReportRound: 1
|
|
})
|
|
|
|
const currentBlogRound = computed(() => {
|
|
const table = formData.value?.depositDetails?.table || []
|
|
let maxRound = 0
|
|
let found = false
|
|
for (const row of table) {
|
|
if ((row.service || '').includes('블로그')) {
|
|
const rNum = parseInt(String(row.round || '').replace(/[^0-9]/g, ''))
|
|
if (!isNaN(rNum) && rNum > maxRound) {
|
|
maxRound = rNum
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
return found ? maxRound : null
|
|
})
|
|
|
|
// 데이터 불러오기 및 정규화
|
|
watch(() => props.company, async (newVal) => {
|
|
if (newVal?.companyId) {
|
|
try {
|
|
const res = await apiClient.get(`/api/report/${newVal.companyId}`)
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
|
|
// 방문자 + 조회수 테이블 병합 로직 (전통적 방식 유지)
|
|
const vTable = data.visitorData?.table || []
|
|
const viewTable = data.viewData?.table || []
|
|
vTable.forEach(vRow => {
|
|
const matching = viewTable.find(viewRow => viewRow.date === vRow.date)
|
|
if (matching) {
|
|
vRow.viewValue = matching.value
|
|
vRow.viewDiff = matching.diff
|
|
vRow.viewIsUp = matching.isUp
|
|
}
|
|
})
|
|
|
|
formData.value = {
|
|
...formData.value,
|
|
...data,
|
|
name: data.name || newVal.name || '',
|
|
visitorData: { ...(data.visitorData || {}), table: vTable },
|
|
companyInfo: data.companyInfo || { manager: '', contact: '', businessNumber: '', naverIdPw: '', blogLink: '' },
|
|
diagnosisResult: data.diagnosisResult || '',
|
|
comparisonData: { '방문자수': '', '조회수': '', '유입변화': '', ...(data.comparisonData || {}) },
|
|
depositDetails: data.depositDetails || { dates: '', table: [] },
|
|
blogReportRound: data.blogReportRound || 1,
|
|
transferInfo: data.transferInfo || { companyName: '(주)이지엠앤씨', businessNumber: '210-81-78652', ceo: '김용수', ourManager: '박민준, 유경미', bankName: '우리은행', account: '1005-704-557550', holder: '(주)이지엠앤씨', amount: '' }
|
|
}
|
|
} else if (res.status === 404) {
|
|
// 데이터가 없는 경우(신규 업체 등) 기본값 유지
|
|
formData.value = {
|
|
...formData.value,
|
|
blogReportRound: 1
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Data Load Error:', err)
|
|
}
|
|
}
|
|
}, { immediate: true })
|
|
|
|
const handleSave = async () => {
|
|
// 저장 시 차트 데이터 자동 생성을 위한 전처리
|
|
formData.value.visitorData.chart = formData.value.visitorData.table
|
|
.filter(item => item.date && item.value)
|
|
.map(item => ({ date: item.date, value: parseInt(String(item.value).replace(/,/g, '')) || 0 }))
|
|
|
|
formData.value.viewData.table = formData.value.visitorData.table
|
|
.filter(item => item.date && item.viewValue)
|
|
.map(item => ({ selected: item.selected, date: item.date, value: item.viewValue, diff: item.viewDiff, isUp: item.viewIsUp }))
|
|
|
|
formData.value.viewData.chart = formData.value.viewData.table
|
|
.map(item => ({ date: item.date, value: parseInt(String(item.value).replace(/,/g, '')) || 0 }))
|
|
|
|
try {
|
|
const res = await apiClient.post('/api/report', {
|
|
companyId: props.company.companyId,
|
|
...formData.value
|
|
})
|
|
if (res.ok) {
|
|
alert('성공적으로 저장되었습니다.')
|
|
emit('saved')
|
|
emit('close')
|
|
}
|
|
} catch (err) {
|
|
alert('저장 중 오류가 발생했습니다.')
|
|
}
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
if (!confirm('정말로 삭제하시겠습니까?')) return
|
|
try {
|
|
const res = await apiClient.delete(`/api/report/${props.company.companyId}`)
|
|
if (res.ok) {
|
|
alert('삭제되었습니다.')
|
|
emit('deleted')
|
|
emit('close')
|
|
}
|
|
} catch (err) {
|
|
alert('삭제 오류')
|
|
}
|
|
}
|
|
|
|
const handleAddNewCompany = async () => {
|
|
const name = prompt('신규 업체명:')
|
|
if (!name) return
|
|
try {
|
|
const res = await apiClient.post('/api/reports/add', { name })
|
|
if (res.ok) {
|
|
alert('추가되었습니다.')
|
|
emit('saved')
|
|
}
|
|
} catch (err) {
|
|
alert('추가 실패')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="show" class="modal-backdrop">
|
|
<div class="modal-content large">
|
|
<header class="modal-header">
|
|
<div class="title-area">
|
|
<FileText class="icon-primary" />
|
|
<div class="name-edit-group">
|
|
<input v-model="formData.name" class="header-name-input" placeholder="업체명">
|
|
<span class="edit-badge">리포트 표시명</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-right-actions">
|
|
<button class="btn-close" @click="emit('close')">
|
|
<X />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="modal-body-layout">
|
|
<aside class="modal-sidebar">
|
|
<button :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">
|
|
<Info :size="18" /> 업체 상세정보
|
|
</button>
|
|
<button :class="{ active: activeTab === 'visitor' }" @click="activeTab = 'visitor'">
|
|
<Activity :size="18" /> 방문자/조회수
|
|
</button>
|
|
<button :class="{ active: activeTab === 'inflow' }" @click="activeTab = 'inflow'">
|
|
<Navigation :size="18" /> 유입 경로 분석
|
|
</button>
|
|
<button :class="{ active: activeTab === 'blog' }" @click="activeTab = 'blog'">
|
|
<Layout :size="18" /> 블로그 현황
|
|
</button>
|
|
<button :class="{ active: activeTab === 'exposure' }" @click="activeTab = 'exposure'">
|
|
<Search :size="18" /> 상위 노출 현황
|
|
</button>
|
|
<button :class="{ active: activeTab === 'plan' }" @click="activeTab = 'plan'">
|
|
<FileText :size="18" /> 차월 마케팅 계획
|
|
</button>
|
|
<button :class="{ active: activeTab === 'summary' }" @click="activeTab = 'summary'">
|
|
<TrendingUp :size="18" /> 종합 성과 분석
|
|
</button>
|
|
<button :class="{ active: activeTab === 'deposit-tracking' }" @click="activeTab = 'deposit-tracking'">
|
|
<Activity :size="18" /> 입금 내역 관리
|
|
</button>
|
|
<button :class="{ active: activeTab === 'transfer' }" @click="activeTab = 'transfer'">
|
|
<CreditCard :size="18" /> 우리 회사 정보
|
|
</button>
|
|
</aside>
|
|
|
|
<section class="tab-content">
|
|
<BasicInfoReporting v-if="activeTab === 'basic'" v-model:name="formData.name"
|
|
v-model:companyInfo="formData.companyInfo" />
|
|
|
|
<VisitorReporting v-if="activeTab === 'visitor'" v-model:visitorData="formData.visitorData" />
|
|
|
|
<InflowReporting v-if="activeTab === 'inflow'" v-model:inflowData="formData.inflowData" />
|
|
|
|
<BlogReporting v-if="activeTab === 'blog'" v-model:blogStatus="formData.blogStatus" />
|
|
|
|
<ExposureReporting v-if="activeTab === 'exposure'" v-model:exposureStatus="formData.exposureStatus" />
|
|
|
|
<PlanReporting v-if="activeTab === 'plan'" v-model:nextMonthPlan="formData.nextMonthPlan" />
|
|
|
|
<SummaryReporting v-if="activeTab === 'summary'" v-model:comparisonData="formData.comparisonData"
|
|
v-model:diagnosisResult="formData.diagnosisResult" v-model:reportSummary="formData.reportSummary"
|
|
:reportRound="currentBlogRound ? currentBlogRound + '회차' : ''" />
|
|
|
|
<DepositTrackingReporting v-if="activeTab === 'deposit-tracking'"
|
|
v-model:depositDetails="formData.depositDetails" />
|
|
|
|
<TransferReporting v-if="activeTab === 'transfer'" v-model:transferInfo="formData.transferInfo" />
|
|
</section>
|
|
</div>
|
|
|
|
<footer class="modal-footer">
|
|
<div class="footer-left-actions">
|
|
<button class="btn-footer-delete" @click="handleDelete">업체 삭제</button>
|
|
<button class="btn-footer-add" @click="handleAddNewCompany">신규 업체 추가</button>
|
|
</div>
|
|
<div class="footer-right-actions">
|
|
<button class="btn-cancel" @click="emit('close')">취소</button>
|
|
<button class="btn-save" @click="handleSave">
|
|
<Save :size="18" /> 전체 저장
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.modal-backdrop {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 2000;
|
|
}
|
|
|
|
.modal-content.large {
|
|
background: white;
|
|
width: 1000px;
|
|
max-width: 95vw;
|
|
height: 85vh;
|
|
border-radius: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.title-area {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.icon-primary {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.name-edit-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border-bottom: 2px solid var(--primary);
|
|
}
|
|
|
|
.header-name-input {
|
|
border: none;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
outline: none;
|
|
width: 250px;
|
|
}
|
|
|
|
.edit-badge {
|
|
font-size: 0.65rem;
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.btn-close {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.modal-body-layout {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-sidebar {
|
|
width: 220px;
|
|
background: #f8fafc;
|
|
border-right: 1px solid #f1f5f9;
|
|
padding: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.modal-sidebar button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
border: none;
|
|
background: none;
|
|
border-radius: 8px;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
color: #64748b;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.modal-sidebar button:hover {
|
|
background: #f1f5f9;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.modal-sidebar button.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.tab-content {
|
|
flex: 1;
|
|
padding: 2.5rem;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 1.5rem;
|
|
border-top: 1px solid #f1f5f9;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.footer-left-actions,
|
|
.footer-right-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn-footer-delete {
|
|
padding: 10px 18px;
|
|
color: #ef4444;
|
|
border: 1px solid #fecaca;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
background: #fff;
|
|
}
|
|
|
|
.btn-footer-add {
|
|
padding: 10px 18px;
|
|
color: #2563eb;
|
|
border: 1px solid #bfdbfe;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
background: #fff;
|
|
}
|
|
|
|
.btn-cancel {
|
|
padding: 10px 20px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
background: #fff;
|
|
}
|
|
|
|
.btn-save {
|
|
padding: 10px 24px;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
</style>
|