import React, { useState, useEffect, useCallback, useRef } from 'react'; import PullToRefresh from '../../components/PullToRefresh'; import { getInstaStatus, instaCollectNews, instaExtractKeywords, getInstaKeywords, createInstaSlate, getInstaSlates, getInstaSlate, renderInstaSlate, deleteInstaSlate, getInstaAssetUrl, getInstaTask, getInstaPrompt, putInstaPrompt, } from '../../api'; import './InstaCards.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 StatusBadge({ status }) { return ( {status || 'draft'} ); } /* ────────────────────── 폴링 훅 ────────────────────── */ 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 getInstaTask(taskId); if (cancelled) return; setTask(t); if (t.status === 'succeeded' || t.status === 'failed') { setTaskId(null); onDone?.(t); } else { timer.current = setTimeout(poll, 3000); } } 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); }, }; } /* ────────────────────── TaskStatusBox ────────────────────── */ function TaskStatusBox({ task }) { if (!task) return null; const pct = task.progress != null ? task.progress : (task.status === 'succeeded' ? 100 : 0); return (
{task.status === 'succeeded' ? '완료' : task.status === 'failed' ? '실패' : '진행 중'}
{task.message || task.error || ''}
); } /* ══════════════════════════════════════════════════════════════════════════ */ export default function InstaCards() { const [status, setStatus] = useState(null); const [selectedSlateId, setSelectedSlateId] = useState(null); const loadStatus = useCallback(() => { return getInstaStatus().then(setStatus).catch(() => {}); }, []); useEffect(() => { loadStatus(); }, [loadStatus]); return (
{/* 헤더 + 상태 배너 */}

Insta Cards

{status && (
Naver {status.naver_api ? 'ON' : 'OFF'} AI {status.anthropic_api ? 'ON' : 'OFF'}
)}
{/* 왼쪽: 트리거 + 키워드 */}
setSelectedSlateId(null)} />
{/* 오른쪽: 슬레이트 목록 + 상세 */}
); } /* ══════════════════════ 트리거 패널 ══════════════════════════════════════ */ function TriggerPanel() { const collectPoll = usePollTask(); const keywordsPoll = usePollTask(); async function handleCollect() { try { const res = await instaCollectNews(); collectPoll.start(res.task_id); } catch (e) { alert('뉴스 수집 실패: ' + e.message); } } async function handleKeywords() { try { const res = await instaExtractKeywords(); keywordsPoll.start(res.task_id); } catch (e) { alert('키워드 추출 실패: ' + e.message); } } const collectBusy = !!collectPoll.taskId; const kwBusy = !!keywordsPoll.taskId; return (

트리거

); } /* ══════════════════════ 키워드 목록 ══════════════════════════════════════ */ const CATEGORIES = ['전체', 'economy', 'psychology', 'celebrity']; function KeywordsPanel({ onCreateSlate }) { const [category, setCategory] = useState('전체'); const [keywords, setKeywords] = useState([]); const [creating, setCreating] = useState(null); // keyword_id being created const slatePoll = usePollTask((t) => { if (t.status === 'succeeded') onCreateSlate?.(); setCreating(null); }); const load = useCallback(() => { const cat = category === '전체' ? undefined : category; getInstaKeywords({ category: cat }).then((r) => setKeywords(r.items || [])).catch(() => {}); }, [category]); useEffect(() => { load(); }, [load]); async function handleCreate(kw) { if (creating) return; setCreating(kw.id); try { const res = await createInstaSlate({ keyword: kw.keyword, category: kw.category, keyword_id: kw.id, }); slatePoll.start(res.task_id); } catch (e) { alert('카드 생성 실패: ' + e.message); setCreating(null); } } return (

트렌딩 키워드

{/* 카테고리 필터 */}
{CATEGORIES.map((c) => ( ))}
{slatePoll.task && } {keywords.length === 0 ? (
키워드가 없습니다. 키워드 추출을 실행하세요.
) : (
{keywords.map((kw) => (
{kw.keyword} {kw.category} · {kw.articles_count ?? 0}건 {kw.score?.toFixed(1) ?? '-'}
))}
)}
); } /* ══════════════════════ 슬레이트 목록 ══════════════════════════════════ */ function SlatesPanel({ selectedId, onSelect }) { const [slates, setSlates] = useState([]); const [detail, setDetail] = useState(null); const loadSlates = useCallback(() => { getInstaSlates(50).then((r) => setSlates(r.items || [])).catch(() => {}); }, []); useEffect(() => { loadSlates(); }, [loadSlates]); useEffect(() => { if (!selectedId) { setDetail(null); return; } getInstaSlate(selectedId).then(setDetail).catch(() => setDetail(null)); }, [selectedId]); function handleSelect(id) { onSelect(id === selectedId ? null : id); } async function handleDelete(id) { if (!confirm('슬레이트를 삭제하시겠습니까?')) return; try { await deleteInstaSlate(id); if (selectedId === id) onSelect(null); loadSlates(); } catch (e) { alert('삭제 실패: ' + e.message); } } async function handleRender(id) { try { const res = await renderInstaSlate(id); // Re-render is fire-and-forget from the panel; user can refresh detail alert('재렌더 요청 완료 (task: ' + res.task_id + ')'); setTimeout(loadSlates, 3000); } catch (e) { alert('재렌더 실패: ' + e.message); } } return (

슬레이트 목록

{slates.length === 0 ? (
슬레이트가 없습니다. 카드를 생성해 보세요.
) : (
{slates.map((s) => (
handleSelect(s.id)} > {s.status === 'rendered' || s.status === 'sent' ? ( {s.keyword} ) : (
🎴
)}
{s.keyword}
{fmtDate(s.created_at)}
))}
)}
{/* 슬레이트 상세 */} {detail && ( handleDelete(detail.id)} onRender={() => handleRender(detail.id)} /> )}
); } /* ══════════════════════ 슬레이트 상세 ══════════════════════════════════ */ function SlateDetail({ slate, onDelete, onRender }) { const pages = slate.assets || []; const pageCount = pages.length > 0 ? pages.length : 10; function copyCaption() { const text = [slate.suggested_caption, slate.hashtags?.join(' ')].filter(Boolean).join('\n\n'); navigator.clipboard.writeText(text).then(() => alert('클립보드에 복사되었습니다!')); } return (
{slate.keyword}
{/* 페이지 이미지 스트립 */} {(slate.status === 'rendered' || slate.status === 'sent') ? (
{Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => ( {`Page ))}
) : (
{slate.status === 'failed' ? '렌더 실패 — 재렌더를 시도하세요.' : '렌더링 전입니다.'}
)} {/* 캡션 */} {slate.suggested_caption && (
캡션
{slate.suggested_caption}
{slate.hashtags?.length > 0 && (
{slate.hashtags.join(' ')}
)}
)} {/* 커버 카피 / 바디 카피 */} {slate.cover_copy && (
커버 카피
{slate.cover_copy}
)}
); } /* ══════════════════════ 프롬프트 템플릿 에디터 ══════════════════════════ */ const PROMPT_NAMES = ['slate_writer', 'category_seeds']; function PromptTemplatesEditor() { const [prompts, setPrompts] = useState({}); const [drafts, setDrafts] = useState({}); const [saving, setSaving] = useState({}); useEffect(() => { PROMPT_NAMES.forEach((name) => { getInstaPrompt(name) .then((p) => { setPrompts((prev) => ({ ...prev, [name]: p })); setDrafts((prev) => ({ ...prev, [name]: p.template })); }) .catch(() => { setPrompts((prev) => ({ ...prev, [name]: null })); setDrafts((prev) => ({ ...prev, [name]: '' })); }); }); }, []); async function handleSave(name) { setSaving((prev) => ({ ...prev, [name]: true })); try { const updated = await putInstaPrompt(name, drafts[name] || '', prompts[name]?.description || ''); setPrompts((prev) => ({ ...prev, [name]: updated })); alert(`${name} 저장 완료`); } catch (e) { alert('저장 실패: ' + e.message); } finally { setSaving((prev) => ({ ...prev, [name]: false })); } } return (

프롬프트 템플릿

{PROMPT_NAMES.map((name) => (
{name} {prompts[name]?.updated_at && ( 최종 수정: {fmtDate(prompts[name].updated_at)} )}
{prompts[name]?.description && (
{prompts[name].description}
)}