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 (
+
+
+
+
+
+ {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 (
+
+ );
+}
+
+/* ══════════════════════ 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: ,