Files
report/frontend/src/components/DataEntryModal.vue
T
2026-07-19 03:44:35 +09:00

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>