fix(insta): SlateDetail JSON 객체 렌더 오류 + 카드 생성 시 자동 스크롤

(1) React error #31 fix: cover_copy/cta_copy는 객체({headline,body,accent_color}),
    body_copies는 배열 — 직접 {slate.cover_copy}로 렌더하면 에러. 필드별로
    분해 렌더하고, 10페이지 전체 카피(커버 1 + 본문 8 + CTA 1)를 detail에
    노출하도록 SlateDetail 확장.

(2) UX: handleCreateSlate 시작 시 window.scrollTo(0, 0)로 상단 progress 배너
    노출 보장. KeywordsPanel의 🎴 버튼도 부모 handleCreateSlate 위임으로
    통합 — Trends/Cards 양쪽 어디서 눌러도 동일 흐름(배너 + 자동 미리보기).

(3) KeywordsPanel의 자체 slatePoll 제거 — 상단 ic-slate-progress 배너로
    일원화하여 중복 진행 표시 회피.
This commit is contained in:
2026-05-17 12:51:26 +09:00
parent 6222b56716
commit aeeab6704f

View File

@@ -337,6 +337,10 @@ export default function InstaCards() {
return;
}
setSlateProgress({ keyword, status: 'starting', message: '카드 생성 시작...' });
// 상단 progress 배너가 보이도록 스크롤 (Trends/Cards 어느 탭의 어느 위치에서 눌렀든)
if (typeof window !== 'undefined') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
try {
const { task_id } = await createInstaSlate({ keyword, category, keyword_id });
let st = null;
@@ -432,7 +436,7 @@ export default function InstaCards() {
<div>
<TriggerPanel />
<div style={{ height: 16 }} />
<KeywordsPanel onCreateSlate={() => setSelectedSlateId(null)} />
<KeywordsPanel onCreateSlate={handleCreateSlate} />
</div>
{/* 오른쪽: 슬레이트 목록 + 상세 */}
@@ -522,10 +526,6 @@ 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;
@@ -534,18 +534,17 @@ function KeywordsPanel({ onCreateSlate }) {
useEffect(() => { load(); }, [load]);
// 부모(InstaCards)의 handleCreateSlate에 위임 — progress 배너 + 스크롤 + 자동 미리보기 공통화
async function handleCreate(kw) {
if (creating) return;
setCreating(kw.id);
try {
const res = await createInstaSlate({
await onCreateSlate?.({
keyword: kw.keyword,
category: kw.category,
keyword_id: kw.id,
});
slatePoll.start(res.task_id);
} catch (e) {
alert('카드 생성 실패: ' + e.message);
} finally {
setCreating(null);
}
}
@@ -567,7 +566,7 @@ function KeywordsPanel({ onCreateSlate }) {
))}
</div>
{slatePoll.task && <TaskStatusBox task={slatePoll.task} />}
{/* progress 표시는 상단 ic-slate-progress 배너에서 일괄 처리 */}
{keywords.length === 0 ? (
<div className="ic-empty">키워드가 없습니다. 키워드 추출을 실행하세요.</div>
@@ -754,11 +753,66 @@ function SlateDetail({ slate, onDelete, onRender }) {
</div>
)}
{/* 커버 카피 / 바디 카피 */}
{slate.cover_copy && (
{/* 커버 카피 (1/10) */}
{slate.cover_copy && typeof slate.cover_copy === 'object' && (
<div className="ic-caption-box">
<div className="ic-caption-box__label">커버 카피</div>
<div className="ic-caption-text">{slate.cover_copy}</div>
<div className="ic-caption-box__label">🎯 커버 (1/10)</div>
<div className="ic-caption-text">
<strong>{slate.cover_copy.headline}</strong>
{slate.cover_copy.body && (
<div style={{ marginTop: 6, opacity: 0.85, whiteSpace: 'pre-wrap' }}>
{slate.cover_copy.body}
</div>
)}
{slate.cover_copy.accent_color && (
<div style={{ marginTop: 6, fontSize: '0.72rem', opacity: 0.5 }}>
accent: <code>{slate.cover_copy.accent_color}</code>
</div>
)}
</div>
</div>
)}
{/* 본문 카피 8장 (2~9/10) */}
{Array.isArray(slate.body_copies) && slate.body_copies.length > 0 && (
<div className="ic-caption-box">
<div className="ic-caption-box__label">📝 본문 8 (2~9/10)</div>
{slate.body_copies.map((b, i) => (
<div
key={i}
style={{
borderTop: i > 0 ? '1px solid rgba(255,255,255,0.06)' : 'none',
padding: '10px 0',
}}
>
<strong>{i + 2}. {b?.headline || ''}</strong>
{b?.body && (
<div style={{ marginTop: 4, opacity: 0.85, whiteSpace: 'pre-wrap' }}>
{b.body}
</div>
)}
</div>
))}
</div>
)}
{/* CTA 카피 (10/10) */}
{slate.cta_copy && typeof slate.cta_copy === 'object' && (
<div className="ic-caption-box">
<div className="ic-caption-box__label">📣 마무리 (10/10)</div>
<div className="ic-caption-text">
<strong>{slate.cta_copy.headline}</strong>
{slate.cta_copy.body && (
<div style={{ marginTop: 6, opacity: 0.85, whiteSpace: 'pre-wrap' }}>
{slate.cta_copy.body}
</div>
)}
{slate.cta_copy.cta && (
<div style={{ marginTop: 8, color: '#ec4899', fontWeight: 700 }}>
CTA: {slate.cta_copy.cta}
</div>
)}
</div>
</div>
)}
</div>