diff --git a/src/pages/insta/InstaCards.jsx b/src/pages/insta/InstaCards.jsx
index 3859da6..1739471 100644
--- a/src/pages/insta/InstaCards.jsx
+++ b/src/pages/insta/InstaCards.jsx
@@ -1,14 +1,8 @@
-import React, { useState, useEffect, useCallback, useRef } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import PullToRefresh from '../../components/PullToRefresh';
import {
getInstaStatus,
createInstaSlate,
- getInstaSlates,
- getInstaSlate,
- renderInstaSlate,
- deleteInstaSlate,
- getInstaAssetUrl,
- instaPackageUrl,
getInstaTask,
getInstaPrompt,
putInstaPrompt,
@@ -19,10 +13,10 @@ import {
} from '../../api';
import './InstaCards.css';
import { fmtDate } from './instaUtils';
-import StatusBadge from './components/StatusBadge';
import TaskStatusBox from './components/TaskStatusBox';
import TriggerPanel from './components/TriggerPanel';
import KeywordsPanel from './components/KeywordsPanel';
+import SlatesPanel from './components/SlatesPanel';
/* ══════════════════════ Trends 탭 패널 1: AccountFocusPanel ══════════════ */
function AccountFocusPanel() {
@@ -394,309 +388,6 @@ export default function InstaCards() {
);
}
-/* ══════════════════════ 슬레이트 목록 ══════════════════════════════════ */
-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}
-
- {fmtDate(s.created_at)}
-
-
-
-
- ))}
-
- )}
-
-
- {/* 슬레이트 상세 */}
- {detail && (
-
handleDelete(detail.id)}
- onRender={() => handleRender(detail.id)}
- />
- )}
-
- );
-}
-
-/* ══════════════════════ 페이지 스트립 (chevron + indicator) ═══════════ */
-function PagesStrip({ slateId, pageCount }) {
- const stripRef = useRef(null);
- const [activePage, setActivePage] = useState(1);
-
- const scrollToPage = useCallback((pageNo) => {
- const strip = stripRef.current;
- if (!strip) return;
- const next = Math.max(1, Math.min(pageCount, pageNo));
- const child = strip.children[next - 1];
- if (child) {
- child.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
- setActivePage(next);
- }
- }, [pageCount]);
-
- // 스크롤/드래그 시 가운데 카드 감지
- const onScroll = useCallback(() => {
- const strip = stripRef.current;
- if (!strip) return;
- const rect = strip.getBoundingClientRect();
- const centerX = rect.left + rect.width / 2;
- let best = 1, bestDist = Infinity;
- Array.from(strip.children).forEach((child, i) => {
- const cRect = child.getBoundingClientRect();
- const cCenter = cRect.left + cRect.width / 2;
- const dist = Math.abs(cCenter - centerX);
- if (dist < bestDist) { bestDist = dist; best = i + 1; }
- });
- if (best !== activePage) setActivePage(best);
- }, [activePage]);
-
- // 키보드 ←/→
- useEffect(() => {
- const onKey = (e) => {
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
- if (e.key === 'ArrowLeft') { scrollToPage(activePage - 1); e.preventDefault(); }
- else if (e.key === 'ArrowRight') { scrollToPage(activePage + 1); e.preventDefault(); }
- };
- window.addEventListener('keydown', onKey);
- return () => window.removeEventListener('keydown', onKey);
- }, [activePage, scrollToPage]);
-
- return (
-
-
-
-
- {Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => (
-

scrollToPage(page)}
- />
- ))}
-
-
-
-
-
-
- {activePage}
- /
- {pageCount}
-
-
-
- );
-}
-
-/* ══════════════════════ 슬레이트 상세 ══════════════════════════════════ */
-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}
-
-
-
-
-
- {/* 페이지 이미지 스트립 (캐러셀: chevron + indicator + ←/→ 키보드) */}
- {(slate.status === 'rendered' || slate.status === 'sent') ? (
-
- ) : (
-
- {slate.status === 'failed' ? '렌더 실패 — 재렌더를 시도하세요.' : '렌더링 전입니다.'}
-
- )}
-
- {/* 캡션 */}
- {slate.suggested_caption && (
-
-
- 캡션
-
-
-
{slate.suggested_caption}
- {slate.hashtags?.length > 0 && (
-
- {slate.hashtags.join(' ')}
-
- )}
-
- )}
-
- {/* 커버 카피 (1/10) */}
- {slate.cover_copy && typeof slate.cover_copy === 'object' && (
-
-
🎯 커버 (1/10)
-
-
{slate.cover_copy.headline}
- {slate.cover_copy.body && (
-
- {slate.cover_copy.body}
-
- )}
- {slate.cover_copy.accent_color && (
-
- accent: {slate.cover_copy.accent_color}
-
- )}
-
-
- )}
-
- {/* 본문 카피 8장 (2~9/10) */}
- {Array.isArray(slate.body_copies) && slate.body_copies.length > 0 && (
-
-
📝 본문 8장 (2~9/10)
- {slate.body_copies.map((b, i) => (
-
0 ? '1px solid rgba(255,255,255,0.06)' : 'none',
- padding: '10px 0',
- }}
- >
-
{i + 2}. {b?.headline || ''}
- {b?.body && (
-
- {b.body}
-
- )}
-
- ))}
-
- )}
-
- {/* CTA 카피 (10/10) */}
- {slate.cta_copy && typeof slate.cta_copy === 'object' && (
-
-
📣 마무리 (10/10)
-
-
{slate.cta_copy.headline}
- {slate.cta_copy.body && (
-
- {slate.cta_copy.body}
-
- )}
- {slate.cta_copy.cta && (
-
- CTA: {slate.cta_copy.cta}
-
- )}
-
-
- )}
-
- );
-}
-
/* ══════════════════════ 프롬프트 템플릿 에디터 ══════════════════════════ */
const PROMPT_NAMES = ['slate_writer', 'category_seeds'];
diff --git a/src/pages/insta/components/PagesStrip.jsx b/src/pages/insta/components/PagesStrip.jsx
new file mode 100644
index 0000000..5441442
--- /dev/null
+++ b/src/pages/insta/components/PagesStrip.jsx
@@ -0,0 +1,89 @@
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { getInstaAssetUrl } from '../../../api';
+
+/* ══════════════════════ 페이지 스트립 (chevron + indicator) ═══════════ */
+function PagesStrip({ slateId, pageCount }) {
+ const stripRef = useRef(null);
+ const [activePage, setActivePage] = useState(1);
+
+ const scrollToPage = useCallback((pageNo) => {
+ const strip = stripRef.current;
+ if (!strip) return;
+ const next = Math.max(1, Math.min(pageCount, pageNo));
+ const child = strip.children[next - 1];
+ if (child) {
+ child.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
+ setActivePage(next);
+ }
+ }, [pageCount]);
+
+ // 스크롤/드래그 시 가운데 카드 감지
+ const onScroll = useCallback(() => {
+ const strip = stripRef.current;
+ if (!strip) return;
+ const rect = strip.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ let best = 1, bestDist = Infinity;
+ Array.from(strip.children).forEach((child, i) => {
+ const cRect = child.getBoundingClientRect();
+ const cCenter = cRect.left + cRect.width / 2;
+ const dist = Math.abs(cCenter - centerX);
+ if (dist < bestDist) { bestDist = dist; best = i + 1; }
+ });
+ if (best !== activePage) setActivePage(best);
+ }, [activePage]);
+
+ // 키보드 ←/→
+ useEffect(() => {
+ const onKey = (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+ if (e.key === 'ArrowLeft') { scrollToPage(activePage - 1); e.preventDefault(); }
+ else if (e.key === 'ArrowRight') { scrollToPage(activePage + 1); e.preventDefault(); }
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [activePage, scrollToPage]);
+
+ return (
+
+
+
+
+ {Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => (
+

scrollToPage(page)}
+ />
+ ))}
+
+
+
+
+
+
+ {activePage}
+ /
+ {pageCount}
+
+
+
+ );
+}
+
+export default PagesStrip;
diff --git a/src/pages/insta/components/SlateDetail.jsx b/src/pages/insta/components/SlateDetail.jsx
new file mode 100644
index 0000000..db6d339
--- /dev/null
+++ b/src/pages/insta/components/SlateDetail.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import StatusBadge from './StatusBadge';
+import PagesStrip from './PagesStrip';
+import { instaPackageUrl } from '../../../api';
+
+/* ══════════════════════ 슬레이트 상세 ══════════════════════════════════ */
+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}
+
+
+
+
+
+ {/* 페이지 이미지 스트립 (캐러셀: chevron + indicator + ←/→ 키보드) */}
+ {(slate.status === 'rendered' || slate.status === 'sent') ? (
+
+ ) : (
+
+ {slate.status === 'failed' ? '렌더 실패 — 재렌더를 시도하세요.' : '렌더링 전입니다.'}
+
+ )}
+
+ {/* 캡션 */}
+ {slate.suggested_caption && (
+
+
+ 캡션
+
+
+
{slate.suggested_caption}
+ {slate.hashtags?.length > 0 && (
+
+ {slate.hashtags.join(' ')}
+
+ )}
+
+ )}
+
+ {/* 커버 카피 (1/10) */}
+ {slate.cover_copy && typeof slate.cover_copy === 'object' && (
+
+
🎯 커버 (1/10)
+
+
{slate.cover_copy.headline}
+ {slate.cover_copy.body && (
+
+ {slate.cover_copy.body}
+
+ )}
+ {slate.cover_copy.accent_color && (
+
+ accent: {slate.cover_copy.accent_color}
+
+ )}
+
+
+ )}
+
+ {/* 본문 카피 8장 (2~9/10) */}
+ {Array.isArray(slate.body_copies) && slate.body_copies.length > 0 && (
+
+
📝 본문 8장 (2~9/10)
+ {slate.body_copies.map((b, i) => (
+
0 ? '1px solid rgba(255,255,255,0.06)' : 'none',
+ padding: '10px 0',
+ }}
+ >
+
{i + 2}. {b?.headline || ''}
+ {b?.body && (
+
+ {b.body}
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* CTA 카피 (10/10) */}
+ {slate.cta_copy && typeof slate.cta_copy === 'object' && (
+
+
📣 마무리 (10/10)
+
+
{slate.cta_copy.headline}
+ {slate.cta_copy.body && (
+
+ {slate.cta_copy.body}
+
+ )}
+ {slate.cta_copy.cta && (
+
+ CTA: {slate.cta_copy.cta}
+
+ )}
+
+
+ )}
+
+ );
+}
+
+export default SlateDetail;
diff --git a/src/pages/insta/components/SlatesPanel.jsx b/src/pages/insta/components/SlatesPanel.jsx
new file mode 100644
index 0000000..9350a71
--- /dev/null
+++ b/src/pages/insta/components/SlatesPanel.jsx
@@ -0,0 +1,102 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import StatusBadge from './StatusBadge';
+import SlateDetail from './SlateDetail';
+import { fmtDate } from '../instaUtils';
+import { getInstaSlates, getInstaSlate, getInstaAssetUrl, renderInstaSlate, deleteInstaSlate } from '../../../api';
+
+/* ══════════════════════ 슬레이트 목록 ══════════════════════════════════ */
+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}
+
+ {fmtDate(s.created_at)}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* 슬레이트 상세 */}
+ {detail && (
+
handleDelete(detail.id)}
+ onRender={() => handleRender(detail.id)}
+ />
+ )}
+
+ );
+}
+
+export default SlatesPanel;