From 06b6717eb9cbaa18cf277551649352b34b3f6180 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 10 Jul 2026 13:45:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(insta):=20SlatesPanel/SlateDetail/Page?= =?UTF-8?q?sStrip=20=EC=B6=94=EC=B6=9C=20(Cards=20=ED=83=AD=20=EC=9A=B0?= =?UTF-8?q?=EC=B8=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/insta/InstaCards.jsx | 313 +-------------------- src/pages/insta/components/PagesStrip.jsx | 89 ++++++ src/pages/insta/components/SlateDetail.jsx | 129 +++++++++ src/pages/insta/components/SlatesPanel.jsx | 102 +++++++ 4 files changed, 322 insertions(+), 311 deletions(-) create mode 100644 src/pages/insta/components/PagesStrip.jsx create mode 100644 src/pages/insta/components/SlateDetail.jsx create mode 100644 src/pages/insta/components/SlatesPanel.jsx 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} - ) : ( -
🎴
- )} -
-
{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) => ( - {`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} - -
-
- - - 📦 패키지 다운로드 (10장 + 캡션) - - -
-
- - {/* 페이지 이미지 스트립 (캐러셀: 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) => ( + {`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} + +
+
+ + + 📦 패키지 다운로드 (10장 + 캡션) + + +
+
+ + {/* 페이지 이미지 스트립 (캐러셀: 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} + ) : ( +
🎴
+ )} +
+
{s.keyword}
+
+ {fmtDate(s.created_at)} + +
+
+
+ ))} +
+ )} +
+ + {/* 슬레이트 상세 */} + {detail && ( + handleDelete(detail.id)} + onRender={() => handleRender(detail.id)} + /> + )} +
+ ); +} + +export default SlatesPanel;