diff --git a/src/api.js b/src/api.js index ec587ad..feb5c8c 100644 --- a/src/api.js +++ b/src/api.js @@ -423,3 +423,86 @@ export function deleteBlogPost(id) { return apiDelete(`/api/blog/posts/${id}`); } +// ── 블로그 마케팅 API ──────────────────────────────────────────────────────── + +export function getBlogMarketingStatus() { + return apiGet('/api/blog-marketing/status'); +} + +export function startResearch(keyword) { + return apiPost('/api/blog-marketing/research', { keyword }); +} + +export function getResearchHistory(limit = 30) { + return apiGet(`/api/blog-marketing/research/history?limit=${limit}`); +} + +export function getResearchDetail(id) { + return apiGet(`/api/blog-marketing/research/${id}`); +} + +export function deleteResearch(id) { + return apiDelete(`/api/blog-marketing/research/${id}`); +} + +export function getBlogMarketingTask(taskId) { + return apiGet(`/api/blog-marketing/task/${encodeURIComponent(taskId)}`); +} + +export function startGenerate(keywordId) { + return apiPost('/api/blog-marketing/generate', { keyword_id: keywordId }); +} + +export function startReview(postId) { + return apiPost(`/api/blog-marketing/review/${postId}`); +} + +export function startRegenerate(postId) { + return apiPost(`/api/blog-marketing/regenerate/${postId}`); +} + +export function getBlogMarketingPosts(status, limit = 50) { + const qs = new URLSearchParams(); + if (status) qs.set('status', status); + if (limit) qs.set('limit', String(limit)); + const q = qs.toString(); + return apiGet(`/api/blog-marketing/posts${q ? '?' + q : ''}`); +} + +export function getBlogMarketingPost(id) { + return apiGet(`/api/blog-marketing/posts/${id}`); +} + +export function updateBlogMarketingPost(id, data) { + return apiPut(`/api/blog-marketing/posts/${id}`, data); +} + +export function deleteBlogMarketingPost(id) { + return apiDelete(`/api/blog-marketing/posts/${id}`); +} + +export function publishBlogMarketingPost(id, naverUrl) { + return apiPost(`/api/blog-marketing/posts/${id}/publish`, { naver_url: naverUrl || '' }); +} + +export function getBlogMarketingCommissions(postId) { + const qs = postId ? `?post_id=${postId}` : ''; + return apiGet(`/api/blog-marketing/commissions${qs}`); +} + +export function addBlogMarketingCommission(data) { + return apiPost('/api/blog-marketing/commissions', data); +} + +export function updateBlogMarketingCommission(id, data) { + return apiPut(`/api/blog-marketing/commissions/${id}`, data); +} + +export function deleteBlogMarketingCommission(id) { + return apiDelete(`/api/blog-marketing/commissions/${id}`); +} + +export function getBlogMarketingDashboard() { + return apiGet('/api/blog-marketing/dashboard'); +} + diff --git a/src/components/Icons.jsx b/src/components/Icons.jsx index 0252100..0a8bc49 100644 --- a/src/components/Icons.jsx +++ b/src/components/Icons.jsx @@ -91,6 +91,17 @@ export const IconSubscription = () => ); +export const IconBlogMarketing = () => + svg( + <> + + + + + + + ); + export const IconBuilding = () => svg( <> diff --git a/src/pages/blog-marketing/BlogMarketing.css b/src/pages/blog-marketing/BlogMarketing.css new file mode 100644 index 0000000..0c2d97e --- /dev/null +++ b/src/pages/blog-marketing/BlogMarketing.css @@ -0,0 +1,138 @@ +/* ── Blog Marketing ─────────────────────────────────────────────────────── */ +.bm { max-width: 1100px; margin: 0 auto; padding: 24px 16px 80px; } + +/* 헤더 */ +.bm-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } +.bm-header h1 { font-size: 1.5rem; font-weight: 700; color: var(--text-primary, #e4e4e7); margin: 0; } +.bm-status { display: flex; gap: 8px; margin-left: auto; } +.bm-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 99px; background: rgba(16,185,129,.15); color: #10b981; } +.bm-badge--off { background: rgba(239,68,68,.12); color: #ef4444; } + +/* 탭 바 */ +.bm-tabs { display: flex; gap: 4px; border-bottom: 1px solid rgba(255,255,255,.08); margin-bottom: 20px; } +.bm-tab { padding: 8px 16px; font-size: 0.85rem; background: none; border: none; color: rgba(255,255,255,.45); cursor: pointer; border-bottom: 2px solid transparent; transition: all .15s; } +.bm-tab:hover { color: rgba(255,255,255,.7); } +.bm-tab--active { color: #10b981; border-bottom-color: #10b981; } + +/* ── Dashboard 탭 ─────────────────────────────────────────────────────────── */ +.bm-dash-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; } +.bm-dash-card { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.06); border-radius: 12px; padding: 16px; } +.bm-dash-card__label { font-size: 0.75rem; color: rgba(255,255,255,.4); margin-bottom: 4px; } +.bm-dash-card__value { font-size: 1.4rem; font-weight: 700; color: var(--text-primary, #e4e4e7); } +.bm-dash-card__value--green { color: #10b981; } + +.bm-dash-section { margin-bottom: 24px; } +.bm-dash-section h3 { font-size: 0.9rem; font-weight: 600; color: rgba(255,255,255,.6); margin-bottom: 12px; } + +.bm-top-posts { display: flex; flex-direction: column; gap: 8px; } +.bm-top-post { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: rgba(255,255,255,.03); border-radius: 8px; } +.bm-top-post__title { font-size: 0.85rem; color: var(--text-primary, #e4e4e7); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bm-top-post__rev { font-size: 0.85rem; font-weight: 600; color: #10b981; margin-left: 12px; white-space: nowrap; } + +/* ── Research 탭 ──────────────────────────────────────────────────────────── */ +.bm-research-form { display: flex; gap: 8px; margin-bottom: 20px; } +.bm-research-input { flex: 1; padding: 10px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(255,255,255,.04); color: var(--text-primary, #e4e4e7); font-size: 0.9rem; outline: none; } +.bm-research-input:focus { border-color: #10b981; } +.bm-research-input::placeholder { color: rgba(255,255,255,.25); } + +.bm-btn { padding: 8px 18px; border-radius: 8px; border: none; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all .15s; display: inline-flex; align-items: center; gap: 6px; } +.bm-btn--primary { background: #10b981; color: #fff; } +.bm-btn--primary:hover { background: #059669; } +.bm-btn--primary:disabled { opacity: .5; cursor: not-allowed; } +.bm-btn--secondary { background: rgba(255,255,255,.08); color: rgba(255,255,255,.7); } +.bm-btn--secondary:hover { background: rgba(255,255,255,.12); } +.bm-btn--danger { background: rgba(239,68,68,.15); color: #ef4444; } +.bm-btn--danger:hover { background: rgba(239,68,68,.25); } +.bm-btn--sm { padding: 4px 10px; font-size: 0.75rem; } + +.bm-spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: bm-spin .6s linear infinite; display: inline-block; } +@keyframes bm-spin { to { transform: rotate(360deg); } } + +/* 분석 카드 */ +.bm-analyses { display: flex; flex-direction: column; gap: 12px; } +.bm-analysis-card { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.06); border-radius: 12px; padding: 16px; } +.bm-analysis-card__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } +.bm-analysis-card__keyword { font-size: 1rem; font-weight: 700; color: var(--text-primary, #e4e4e7); } +.bm-analysis-card__date { font-size: 0.7rem; color: rgba(255,255,255,.3); } +.bm-analysis-card__scores { display: flex; gap: 16px; margin-bottom: 10px; flex-wrap: wrap; } +.bm-score { text-align: center; } +.bm-score__label { font-size: 0.65rem; color: rgba(255,255,255,.4); display: block; margin-bottom: 2px; } +.bm-score__value { font-size: 1.1rem; font-weight: 700; } +.bm-score__value--high { color: #10b981; } +.bm-score__value--mid { color: #fbbf24; } +.bm-score__value--low { color: #ef4444; } +.bm-analysis-card__summary { font-size: 0.8rem; color: rgba(255,255,255,.5); line-height: 1.5; } +.bm-analysis-card__actions { display: flex; gap: 8px; margin-top: 12px; } + +/* ── Write 탭 ─────────────────────────────────────────────────────────────── */ +.bm-write-empty { text-align: center; padding: 60px 20px; color: rgba(255,255,255,.3); } +.bm-write-empty p { font-size: 0.85rem; margin-top: 8px; } + +.bm-progress { margin-bottom: 20px; } +.bm-progress__bar { height: 4px; background: rgba(255,255,255,.08); border-radius: 2px; overflow: hidden; margin-bottom: 6px; } +.bm-progress__fill { height: 100%; background: #10b981; border-radius: 2px; transition: width .3s; } +.bm-progress__text { font-size: 0.75rem; color: rgba(255,255,255,.4); } + +.bm-preview { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.06); border-radius: 12px; padding: 20px; margin-bottom: 16px; } +.bm-preview__title { font-size: 1.1rem; font-weight: 700; color: var(--text-primary, #e4e4e7); margin-bottom: 12px; } +.bm-preview__body { font-size: 0.85rem; color: rgba(255,255,255,.6); line-height: 1.7; max-height: 400px; overflow-y: auto; } +.bm-preview__body h1, .bm-preview__body h2, .bm-preview__body h3 { color: var(--text-primary, #e4e4e7); margin: 16px 0 8px; } +.bm-preview__body table { width: 100%; border-collapse: collapse; margin: 12px 0; } +.bm-preview__body th, .bm-preview__body td { border: 1px solid rgba(255,255,255,.1); padding: 6px 10px; font-size: 0.8rem; } +.bm-preview__body th { background: rgba(255,255,255,.06); } +.bm-preview__tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; } +.bm-tag { font-size: 0.7rem; padding: 2px 8px; border-radius: 4px; background: rgba(16,185,129,.12); color: #10b981; } + +.bm-review-box { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.06); border-radius: 12px; padding: 16px; margin-bottom: 16px; } +.bm-review-box h4 { font-size: 0.85rem; font-weight: 600; color: var(--text-primary, #e4e4e7); margin-bottom: 10px; } +.bm-review-scores { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; } +.bm-review-score { text-align: center; min-width: 60px; } +.bm-review-score__label { font-size: 0.65rem; color: rgba(255,255,255,.4); display: block; } +.bm-review-score__val { font-size: 1rem; font-weight: 700; } +.bm-review-total { font-size: 0.85rem; font-weight: 700; margin-bottom: 6px; } +.bm-review-total--pass { color: #10b981; } +.bm-review-total--fail { color: #ef4444; } +.bm-review-feedback { font-size: 0.8rem; color: rgba(255,255,255,.5); line-height: 1.5; } + +.bm-write-actions { display: flex; gap: 8px; flex-wrap: wrap; } + +/* ── Posts 탭 ─────────────────────────────────────────────────────────────── */ +.bm-posts-filter { display: flex; gap: 4px; margin-bottom: 16px; } +.bm-filter-btn { padding: 4px 12px; border-radius: 6px; border: none; font-size: 0.75rem; background: rgba(255,255,255,.06); color: rgba(255,255,255,.5); cursor: pointer; transition: all .15s; } +.bm-filter-btn--active { background: rgba(16,185,129,.15); color: #10b981; } + +.bm-posts-list { display: flex; flex-direction: column; gap: 10px; } +.bm-post-card { background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.06); border-radius: 12px; padding: 14px 16px; } +.bm-post-card__top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; } +.bm-post-card__title { font-size: 0.9rem; font-weight: 600; color: var(--text-primary, #e4e4e7); flex: 1; } +.bm-post-card__status { font-size: 0.65rem; padding: 2px 8px; border-radius: 4px; font-weight: 600; white-space: nowrap; margin-left: 8px; } +.bm-post-card__status--draft { background: rgba(255,255,255,.08); color: rgba(255,255,255,.5); } +.bm-post-card__status--reviewed { background: rgba(96,165,250,.15); color: #60a5fa; } +.bm-post-card__status--published { background: rgba(16,185,129,.15); color: #10b981; } +.bm-post-card__excerpt { font-size: 0.8rem; color: rgba(255,255,255,.4); margin-bottom: 8px; line-height: 1.4; } +.bm-post-card__meta { font-size: 0.7rem; color: rgba(255,255,255,.25); display: flex; gap: 12px; } +.bm-post-card__actions { display: flex; gap: 6px; margin-top: 10px; } + +/* 발행 모달 */ +.bm-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 100; display: flex; align-items: center; justify-content: center; } +.bm-modal { background: #1e1e24; border: 1px solid rgba(255,255,255,.1); border-radius: 14px; padding: 24px; width: 90%; max-width: 440px; } +.bm-modal h3 { font-size: 1rem; font-weight: 700; color: var(--text-primary, #e4e4e7); margin-bottom: 12px; } +.bm-modal__input { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(255,255,255,.04); color: var(--text-primary, #e4e4e7); font-size: 0.85rem; outline: none; margin-bottom: 14px; } +.bm-modal__input:focus { border-color: #10b981; } +.bm-modal__buttons { display: flex; gap: 8px; justify-content: flex-end; } + +/* ── 공통 빈 상태 ─────────────────────────────────────────────────────────── */ +.bm-empty { text-align: center; padding: 48px 20px; color: rgba(255,255,255,.25); font-size: 0.85rem; } + +/* ── 모바일 ───────────────────────────────────────────────────────────────── */ +@media (max-width: 640px) { + .bm { padding: 16px 10px 60px; } + .bm-header h1 { font-size: 1.2rem; } + .bm-status { display: none; } + .bm-tab { padding: 6px 10px; font-size: 0.8rem; } + .bm-dash-cards { grid-template-columns: repeat(2, 1fr); } + .bm-research-form { flex-direction: column; } + .bm-analysis-card__scores { gap: 10px; } + .bm-write-actions { flex-direction: column; } + .bm-post-card__actions { flex-wrap: wrap; } +} diff --git a/src/pages/blog-marketing/BlogMarketing.jsx b/src/pages/blog-marketing/BlogMarketing.jsx new file mode 100644 index 0000000..6473a5f --- /dev/null +++ b/src/pages/blog-marketing/BlogMarketing.jsx @@ -0,0 +1,566 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { + getBlogMarketingStatus, + startResearch, + getResearchHistory, + getResearchDetail, + deleteResearch, + getBlogMarketingTask, + startGenerate, + startReview, + startRegenerate, + getBlogMarketingPosts, + getBlogMarketingPost, + deleteBlogMarketingPost, + publishBlogMarketingPost, + getBlogMarketingDashboard, + getBlogMarketingCommissions, + addBlogMarketingCommission, + deleteBlogMarketingCommission, +} 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 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); + + useEffect(() => { + getBlogMarketingStatus().then(setStatus).catch(() => {}); + }, []); + + 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' && { setTab('write'); }} />} + {tab === 'write' && } + {tab === 'posts' && } +
+ ); +} + +/* ══════════════════════ 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 loadDrafts = useCallback(() => { + getBlogMarketingPosts('draft', 20).then(r => { + const drafts = r.posts || []; + setPosts(drafts); + if (drafts.length > 0 && !selected) setSelected(drafts[0].id); + }).catch(() => {}); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { loadDrafts(); }, [loadDrafts]); + + useEffect(() => { + if (!selected) { setPost(null); return; } + getBlogMarketingPost(selected).then(setPost).catch(() => {}); + }, [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 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 handleCopy = () => { + if (!post) return; + navigator.clipboard.writeText(post.body).then(() => alert('본문이 클립보드에 복사되었습니다!')); + }; + + const activePoll = reviewPoll.task || regenPoll.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 && ( + <> +
+
{post.title || '(제목 없음)'}
+
+ {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}/50 {post.review_detail.pass ? '(통과)' : '(미달)'} +
+ {post.review_detail.feedback && ( +
{post.review_detail.feedback}
+ )} +
+ )} + +
+ + + +
+ + )} +
+ ); +} + +/* ══════════════════════ 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) => { + navigator.clipboard.writeText(body).then(() => alert('복사 완료!')); + }; + + const filters = [ + { id: '', label: '전체' }, + { id: 'draft', label: 'Draft' }, + { 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}/50} + {p.naver_url && 네이버 링크} + {fmtDate(p.created_at)} +
+
+ + {p.status !== 'published' && ( + + )} + +
+
+ ))} +
+ + {publishModal && ( +
setPublishModal(null)}> +
e.stopPropagation()}> +

네이버 블로그 발행

+

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

+ setNaverUrl(e.target.value)} + /> +
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/routes.jsx b/src/routes.jsx index 8f0d8e5..0a8cfd9 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -9,6 +9,7 @@ import { IconMusic, IconLab, IconTodo, + IconBlogMarketing, } from './components/Icons'; const Home = lazy(() => import('./pages/home/Home')); @@ -24,6 +25,7 @@ const SwordStream = lazy(() => import('./pages/effect-lab/SwordStream')); const DayCalc = lazy(() => import('./pages/effect-lab/DayCalc')); const Todo = lazy(() => import('./pages/todo/Todo')); const MusicStudio = lazy(() => import('./pages/music/MusicStudio')); +const BlogMarketing = lazy(() => import('./pages/blog-marketing/BlogMarketing')); export const navLinks = [ { @@ -89,6 +91,15 @@ export const navLinks = [ icon: , accent: '#f5a623', }, + { + id: 'blog-lab', + label: 'Blog Lab', + path: '/blog-lab', + subtitle: 'MONETIZE', + description: 'AI 블로그 마케팅으로 수익을 만드는 연구소', + icon: , + accent: '#10b981', + }, { id: 'lab', label: 'Lab', @@ -158,6 +169,10 @@ export const appRoutes = [ path: 'music', element: , }, + { + path: 'blog-lab', + element: , + }, { path: 'todo', element: ,