import React, { useState, useEffect, useCallback, useRef } from 'react'; import PullToRefresh from '../../components/PullToRefresh'; import FAB from '../../components/FAB'; import { getBlogMarketingStatus, startResearch, getResearchHistory, getResearchDetail, deleteResearch, getBlogMarketingTask, startGenerate, startReview, startRegenerate, startMarket, getBlogMarketingPosts, getBlogMarketingPost, deleteBlogMarketingPost, publishBlogMarketingPost, getBlogMarketingDashboard, getBlogMarketingCommissions, addBlogMarketingCommission, deleteBlogMarketingCommission, getBrandLinks, createBrandLink, deleteBrandLink, } from '../../api'; import './BlogMarketing.css'; /* ────────────────────── 유틸 ────────────────────── */ function fmtDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function fmtMoney(n) { if (n == null) return '-'; return n.toLocaleString('ko-KR') + '원'; } function copyHtmlToClipboard(html) { const blob = new Blob([html], { type: 'text/html' }); const plainBlob = new Blob([html.replace(/<[^>]*>/g, '')], { type: 'text/plain' }); navigator.clipboard.write([ new ClipboardItem({ 'text/html': blob, 'text/plain': plainBlob }), ]).then(() => alert('본문이 클립보드에 복사되었습니다! (서식 포함)')); } function scoreColor(v, max = 100) { const r = v / max; if (r >= 0.6) return 'bm-score__value--high'; if (r >= 0.3) return 'bm-score__value--mid'; return 'bm-score__value--low'; } /* ────────────────────── 폴링 훅 ────────────────────── */ function usePollTask(onDone) { const [taskId, setTaskId] = useState(null); const [task, setTask] = useState(null); const timer = useRef(null); useEffect(() => { if (!taskId) return; let cancelled = false; const poll = async () => { try { const t = await getBlogMarketingTask(taskId); if (cancelled) return; setTask(t); if (t.status === 'succeeded' || t.status === 'failed') { setTaskId(null); onDone?.(t); } else { timer.current = setTimeout(poll, 1500); } } catch { if (!cancelled) timer.current = setTimeout(poll, 3000); } }; poll(); return () => { cancelled = true; clearTimeout(timer.current); }; }, [taskId]); // eslint-disable-line react-hooks/exhaustive-deps return { taskId, task, start: setTaskId, clear: () => { setTaskId(null); setTask(null); } }; } /* ══════════════════════════════════════════════════════════════════════════ */ export default function BlogMarketing() { const [tab, setTab] = useState('dashboard'); const [status, setStatus] = useState(null); const loadStatus = useCallback(() => { return getBlogMarketingStatus().then(setStatus).catch(() => {}); }, []); useEffect(() => { loadStatus(); }, [loadStatus]); const tabs = [ { id: 'dashboard', label: 'Dashboard' }, { id: 'research', label: 'Research' }, { id: 'write', label: 'Write' }, { id: 'posts', label: 'Posts' }, ]; return (

Blog Lab

{status && (
Naver {status.naver_api ? 'ON' : 'OFF'} Claude {status.claude_api ? 'ON' : 'OFF'}
)}
{tab === 'dashboard' && } {tab === 'research' && } {tab === 'write' && } {tab === 'posts' && } setTab('research')} label="키워드 분석" />
); } /* ══════════════════════ Dashboard 탭 ═════════════════════════════════════ */ function DashboardTab() { const [data, setData] = useState(null); useEffect(() => { getBlogMarketingDashboard().then(setData).catch(() => {}); }, []); if (!data) return
로딩 중...
; return (
{data.top_posts?.length > 0 && (

Top 5 포스트 (수익 기준)

{data.top_posts.map(p => (
{p.title || '(제목 없음)'} {fmtMoney(p.total_revenue)}
))}
)} {data.monthly?.length > 0 && (

월별 수익

{data.monthly.map(m => (
{m.month} 클릭 {m.clicks} / 구매 {m.purchases} {fmtMoney(m.revenue)}
))}
)}
); } function DashCard({ label, value, green }) { return (
{label}
{value}
); } /* ══════════════════════ Research 탭 ══════════════════════════════════════ */ function ResearchTab() { const [keyword, setKeyword] = useState(''); const [analyses, setAnalyses] = useState([]); const [expanded, setExpanded] = useState(null); const loadHistory = useCallback(() => { getResearchHistory(30).then(r => setAnalyses(r.analyses || [])).catch(() => {}); }, []); useEffect(() => { loadHistory(); }, [loadHistory]); const poll = usePollTask((t) => { if (t.status === 'succeeded') loadHistory(); }); const handleSearch = async () => { if (!keyword.trim() || poll.taskId) return; try { const { task_id } = await startResearch(keyword.trim()); poll.start(task_id); } catch (e) { alert(e.message); } }; const handleDelete = async (id) => { if (!confirm('이 분석을 삭제할까요?')) return; await deleteResearch(id); setAnalyses(prev => prev.filter(a => a.id !== id)); }; const handleGenerate = async (analysisId) => { try { const { task_id } = await startGenerate(analysisId); alert(`글 생성 시작! (task: ${task_id.slice(0, 8)})\nWrite 탭에서 확인하세요.`); } catch (e) { alert(e.message); } }; return (
setKeyword(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} disabled={!!poll.taskId} />
{poll.task && poll.task.status !== 'succeeded' && poll.task.status !== 'failed' && (
{poll.task.message || '처리 중...'}
)}
{analyses.length === 0 && !poll.taskId && (
아직 분석 결과가 없습니다. 키워드를 입력해 첫 분석을 시작하세요!
)} {analyses.map(a => (
{a.keyword} {fmtDate(a.created_at)}
경쟁도 {a.competition}
기회 {a.opportunity}
블로그 {(a.blog_total || 0).toLocaleString()}
쇼핑 {(a.shop_total || 0).toLocaleString()}
{a.avg_price != null && (
평균가 {fmtMoney(a.avg_price)}
)}
{expanded === a.id && a.top_products?.length > 0 && (
상위 상품:
    {a.top_products.map((p, i) => (
  • {p.title} — {fmtMoney(p.lprice)} ({p.mallName})
  • ))}
)}
))}
); } /* ══════════════════════ Write 탭 ═════════════════════════════════════════ */ function WriteTab() { const [posts, setPosts] = useState([]); const [selected, setSelected] = useState(null); const [post, setPost] = useState(null); // 브랜드 링크 상태 const [links, setLinks] = useState([]); const [showLinkForm, setShowLinkForm] = useState(false); const [linkForm, setLinkForm] = useState({ url: '', product_name: '', description: '', placement_hint: '' }); const loadPosts = useCallback(() => { Promise.all([ getBlogMarketingPosts('draft', 20), getBlogMarketingPosts('marketed', 20), ]).then(([draftRes, marketedRes]) => { const all = [...(draftRes.posts || []), ...(marketedRes.posts || [])]; all.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); setPosts(all); if (all.length > 0 && !selected) setSelected(all[0].id); }).catch(() => {}); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { loadPosts(); }, [loadPosts]); useEffect(() => { if (!selected) { setPost(null); setLinks([]); return; } getBlogMarketingPost(selected).then(setPost).catch(() => {}); getBrandLinks({ post_id: selected }).then(r => setLinks(r.links || [])).catch(() => setLinks([])); }, [selected]); const reviewPoll = usePollTask((t) => { if (t.status === 'succeeded' && t.result_id) { getBlogMarketingPost(t.result_id).then(setPost).catch(() => {}); } }); const regenPoll = usePollTask((t) => { if (t.status === 'succeeded' && t.result_id) { getBlogMarketingPost(t.result_id).then(setPost).catch(() => {}); } }); const marketPoll = usePollTask((t) => { if (t.status === 'succeeded' && t.result_id) { getBlogMarketingPost(t.result_id).then(setPost).catch(() => {}); loadPosts(); } }); const handleReview = async () => { if (!post) return; try { const { task_id } = await startReview(post.id); reviewPoll.start(task_id); } catch (e) { alert(e.message); } }; const handleRegenerate = async () => { if (!post) return; try { const { task_id } = await startRegenerate(post.id); regenPoll.start(task_id); } catch (e) { alert(e.message); } }; const handleMarket = async () => { if (!post) return; if (links.length === 0) { alert('마케터 실행 전 브랜드커넥트 링크를 먼저 추가하세요.'); return; } try { const { task_id } = await startMarket(post.id); marketPoll.start(task_id); } catch (e) { alert(e.message); } }; const handleCopy = () => { if (!post) return; copyHtmlToClipboard(post.body); }; const handleAddLink = async () => { if (!linkForm.url.trim() || !linkForm.product_name.trim()) { alert('URL과 상품명은 필수입니다.'); return; } try { await createBrandLink({ ...linkForm, post_id: selected }); setLinkForm({ url: '', product_name: '', description: '', placement_hint: '' }); setShowLinkForm(false); getBrandLinks({ post_id: selected }).then(r => setLinks(r.links || [])).catch(() => {}); } catch (e) { alert(e.message); } }; const handleDeleteLink = async (linkId) => { if (!confirm('이 링크를 삭제할까요?')) return; await deleteBrandLink(linkId); setLinks(prev => prev.filter(l => l.id !== linkId)); }; const activePoll = reviewPoll.task || regenPoll.task || marketPoll.task; const isProcessing = activePoll && activePoll.status !== 'succeeded' && activePoll.status !== 'failed'; if (posts.length === 0 && !post) { return (

아직 작성 중인 글이 없습니다.
Research 탭에서 키워드를 분석하고 글 생성을 시작하세요.

); } return (
{posts.length > 1 && (
{posts.map(p => ( ))}
)} {isProcessing && activePoll && (
{activePoll.message || '처리 중...'}
)} {post && ( <> {/* 브랜드커넥트 링크 섹션 */}

브랜드커넥트 링크 ({links.length})

{showLinkForm && (
setLinkForm(p => ({ ...p, url: e.target.value }))} style={{ fontSize: '0.85rem' }} /> setLinkForm(p => ({ ...p, product_name: e.target.value }))} style={{ fontSize: '0.85rem' }} /> setLinkForm(p => ({ ...p, description: e.target.value }))} style={{ fontSize: '0.85rem' }} /> setLinkForm(p => ({ ...p, placement_hint: e.target.value }))} style={{ fontSize: '0.85rem' }} />
)} {links.length > 0 && (
{links.map(l => (
{l.product_name} {l.description && {l.description}}
))}
)}
{post.title || '(제목 없음)'}
{post.status}
{post.tags?.length > 0 && (
{post.tags.map((t, i) => #{t})}
)}
{post.review_detail && post.review_score != null && (

품질 리뷰 결과

{Object.entries(post.review_detail.scores || {}).map(([k, v]) => (
{k} {v}
))}
총점: {post.review_score}/60 {post.review_detail.pass ? '(통과)' : '(미달)'}
{post.review_detail.feedback && (
{post.review_detail.feedback}
)}
)}
{post.status === 'draft' && ( )}
)}
); } /* ══════════════════════ Posts 탭 ═════════════════════════════════════════ */ function PostsTab() { const [filter, setFilter] = useState(''); const [posts, setPosts] = useState([]); const [publishModal, setPublishModal] = useState(null); const [naverUrl, setNaverUrl] = useState(''); const load = useCallback(() => { getBlogMarketingPosts(filter || undefined).then(r => setPosts(r.posts || [])).catch(() => {}); }, [filter]); useEffect(() => { load(); }, [load]); const handleDelete = async (id) => { if (!confirm('이 포스트를 삭제할까요?')) return; await deleteBlogMarketingPost(id); setPosts(prev => prev.filter(p => p.id !== id)); }; const handlePublish = async () => { if (!publishModal) return; await publishBlogMarketingPost(publishModal, naverUrl); setPublishModal(null); setNaverUrl(''); load(); }; const handleCopy = (body) => { copyHtmlToClipboard(body); }; const filters = [ { id: '', label: '전체' }, { id: 'draft', label: 'Draft' }, { id: 'marketed', label: 'Marketed' }, { id: 'reviewed', label: 'Reviewed' }, { id: 'published', label: 'Published' }, ]; return (
{filters.map(f => ( ))}
{posts.length === 0 &&
포스트가 없습니다.
} {posts.map(p => (
{p.title || '(제목 없음)'} {p.status}
{p.excerpt &&
{p.excerpt}
}
{p.review_score != null && 리뷰: {p.review_score}/60} {p.naver_url && 네이버 링크} {fmtDate(p.created_at)}
{p.status !== 'published' && ( )}
))}
{publishModal && (
setPublishModal(null)}>
e.stopPropagation()}>

네이버 블로그 발행

본문을 네이버 블로그에 붙여넣기한 후, 발행된 URL을 입력하세요.

setNaverUrl(e.target.value)} />
)}
); }