Files
landing-manager/frontend/app/pages/admin/leads.vue
2026-03-05 10:35:28 +09:00

214 lines
8.4 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
interface LeadRecord {
id: string
name: string
phone: string
email: string
campaign: string
page: string
channel: string
status: '새리드' | '검토중' | '완료'
createdAt: string
}
const leads = ref<LeadRecord[]>([
{
id: 'ld-1001',
name: '김하늘',
phone: '010-1111-2222',
email: 'haneul@example.com',
campaign: 'Q3 마케팅 캠페인',
page: '메인 랜딩',
channel: 'Google',
status: '새리드',
createdAt: '2026-03-04T09:12:00.000Z'
},
{
id: 'ld-1002',
name: '이준수',
phone: '010-2222-3333',
email: 'junsoo@example.com',
campaign: '블랙프라이데이 2023',
page: '구글 전용 랜딩',
channel: 'Meta',
status: '검토중',
createdAt: '2026-03-03T17:44:00.000Z'
},
{
id: 'ld-1003',
name: '박소영',
phone: '010-3333-4444',
email: 'soyoung@example.com',
campaign: 'Product Hunt 런칭',
page: '리타겟 랜딩',
channel: 'Instagram',
status: '완료',
createdAt: '2026-03-02T21:13:00.000Z'
},
{
id: 'ld-1004',
name: '최민재',
phone: '010-4444-5555',
email: 'minjae@example.com',
campaign: 'SaaS 웨비나 시리즈',
page: '오퍼 페이지 B',
channel: 'Naver',
status: '새리드',
createdAt: '2026-03-01T11:08:00.000Z'
},
{
id: 'ld-1005',
name: '정연우',
phone: '010-5555-6666',
email: 'yeonwoo@example.com',
campaign: 'Q3 마케팅 캠페인',
page: '메인 랜딩',
channel: 'Google',
status: '검토중',
createdAt: '2026-03-01T03:03:00.000Z'
}
])
const searchQuery = ref('')
const statusFilter = ref<'all' | LeadRecord['status']>('all')
const channelFilter = ref<'all' | string>('all')
const campaignFilter = ref<'all' | string>('all')
const channelOptions = ['Google', 'Meta', 'Instagram', 'Naver']
const campaigns = computed(() => {
const names = [...new Set(leads.value.map((lead) => lead.campaign))]
return names
})
const filteredLeads = computed(() => {
const query = searchQuery.value.trim().toLowerCase()
return leads.value
.filter((lead) => {
const target = `${lead.name} ${lead.phone} ${lead.email} ${lead.campaign} ${lead.page} ${lead.channel} ${lead.status}`.toLowerCase()
const matchesQuery = query === '' || target.includes(query)
const matchesStatus = statusFilter.value === 'all' || lead.status === statusFilter.value
const matchesChannel = channelFilter.value === 'all' || lead.channel === channelFilter.value
const matchesCampaign = campaignFilter.value === 'all' || lead.campaign === campaignFilter.value
return matchesQuery && matchesStatus && matchesChannel && matchesCampaign
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
})
const statusLabel = (status: LeadRecord['status']) => {
if (status === '완료') return '완료'
if (status === '검토중') return '검토중'
return '새 리드'
}
const statusClass = (status: LeadRecord['status']) => {
if (status === '완료') {
return 'text-emerald-200 bg-emerald-400/10'
}
if (status === '검토중') {
return 'text-amber-200 bg-amber-400/10'
}
return 'text-indigo-200 bg-indigo-400/10'
}
</script>
<template>
<div class="space-y-5">
<header class="rounded-xl border border-slate-700/80 bg-gradient-to-r from-slate-900 via-slate-900 to-indigo-950/70 px-5 py-4 shadow-[0_16px_40px_rgba(0,0,0,0.35)]">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-sm text-slate-400"> <span class="px-2">></span> 리드 조회</p>
<h1 class="mt-1 text-3xl font-black tracking-tight text-slate-100">리드 조회</h1>
<p class="mt-1 text-sm text-slate-400">캠페인/페이지 기반으로 리드를 확인하고 상태를 관리하세요.</p>
</div>
<button class="rounded-xl bg-gradient-to-r from-indigo-500 to-cyan-500 px-5 py-2.5 text-base font-semibold text-white shadow-lg shadow-cyan-950/40 transition hover:from-indigo-400 hover:to-cyan-400">
리드 엑셀 내보내기
</button>
</div>
</header>
<section class="rounded-2xl border border-indigo-400/20 bg-gradient-to-br from-slate-900/80 via-slate-950 to-slate-900/90 p-5 shadow-[0_12px_30px_rgba(0,0,0,0.3)]">
<div class="flex flex-nowrap items-end justify-between gap-4">
<label class="min-w-0 space-y-2">
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">검색</span>
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-3 flex items-center text-cyan-300"></span>
<input
v-model="searchQuery"
type="text"
placeholder="이름, 전화번호, 이메일, 캠페인, 페이지"
class="w-full min-w-[420px] rounded-xl border border-indigo-500/20 bg-slate-950/80 px-9 py-3 text-sm text-slate-100 outline-none ring-0 transition focus:border-cyan-400 focus:shadow-[0_0_0_1px_rgba(103,232,249,0.35)]"
/>
</div>
</label>
<div class="ml-auto flex shrink-0 items-end gap-3">
<label class="flex flex-col space-y-2">
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">상태</span>
<select v-model="statusFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
<option value="all">전체</option>
<option value="새리드"> 리드</option>
<option value="검토중">검토중</option>
<option value="완료">완료</option>
</select>
</label>
<label class="flex flex-col space-y-2">
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">채널</span>
<select v-model="channelFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
<option value="all">전체</option>
<option v-for="ch in channelOptions" :key="ch" :value="ch">
{{ ch }}
</option>
</select>
</label>
<label class="flex flex-col space-y-2">
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">캠페인</span>
<select v-model="campaignFilter" class="rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400">
<option value="all">전체 캠페인</option>
<option v-for="name in campaigns" :key="name" :value="name">{{ name }}</option>
</select>
</label>
</div>
</div>
</section>
<section class="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950/80">
<table class="w-full text-left text-sm">
<thead class="bg-slate-900 text-slate-300">
<tr>
<th class="px-4 py-3">이름</th>
<th class="px-4 py-3">연락처</th>
<th class="px-4 py-3">이메일</th>
<th class="px-4 py-3">캠페인</th>
<th class="px-4 py-3">페이지</th>
<th class="px-4 py-3">채널</th>
<th class="px-4 py-3">상태</th>
<th class="px-4 py-3">수집일</th>
</tr>
</thead>
<tbody>
<tr v-for="lead in filteredLeads" :key="lead.id" class="border-t border-slate-800 text-slate-200">
<td class="px-4 py-3">{{ lead.name }}</td>
<td class="px-4 py-3">{{ lead.phone }}</td>
<td class="px-4 py-3 text-cyan-200">{{ lead.email }}</td>
<td class="px-4 py-3">{{ lead.campaign }}</td>
<td class="px-4 py-3">{{ lead.page }}</td>
<td class="px-4 py-3">{{ lead.channel }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold tracking-[0.07em]" :class="statusClass(lead.status)">
{{ statusLabel(lead.status) }}
</span>
</td>
<td class="px-4 py-3">{{ new Date(lead.createdAt).toLocaleString() }}</td>
</tr>
</tbody>
</table>
</section>
</div>
</template>