diff --git a/src/pages/insta/InstaCards.jsx b/src/pages/insta/InstaCards.jsx
index 1739471..3cae41e 100644
--- a/src/pages/insta/InstaCards.jsx
+++ b/src/pages/insta/InstaCards.jsx
@@ -4,219 +4,15 @@ import {
getInstaStatus,
createInstaSlate,
getInstaTask,
- getInstaPrompt,
- putInstaPrompt,
- getInstaTrends,
- instaCollectTrends,
- getInstaPreferences,
- putInstaPreferences,
} from '../../api';
import './InstaCards.css';
-import { fmtDate } from './instaUtils';
-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() {
- const [prefs, setPrefs] = useState([]);
- const [draft, setDraft] = useState({});
- const [saving, setSaving] = useState(false);
- const [newCat, setNewCat] = useState('');
-
- const load = useCallback(async () => {
- const data = await getInstaPreferences();
- setPrefs(data.categories || []);
- const m = {};
- (data.categories || []).forEach(p => { m[p.category] = Math.round(p.weight * 100); });
- setDraft(m);
- }, []);
-
- useEffect(() => { load(); }, [load]);
-
- const save = async () => {
- setSaving(true);
- try {
- const payload = {};
- Object.entries(draft).forEach(([k, v]) => { payload[k] = (Number(v) || 0) / 100; });
- await putInstaPreferences(payload);
- await load();
- } finally { setSaving(false); }
- };
-
- const addCat = () => {
- const name = newCat.trim().toLowerCase();
- if (!name || draft[name] !== undefined) return;
- setDraft({ ...draft, [name]: 0 });
- setNewCat('');
- };
-
- return (
-
- 🎯 이 계정의 주제 (카테고리 가중치)
- 슬라이더는 각 카테고리에 자동 추출 키워드 비율을 결정합니다. 합계는 자동 정규화됩니다.
-
- {Object.entries(draft).map(([cat, val]) => (
-
-
- setDraft({ ...draft, [cat]: Number(e.target.value) })}
- className="ic-focus__slider"
- />
- {val}%
-
- ))}
-
-
- setNewCat(e.target.value)}
- />
-
-
-
-
- 💡 신규 카테고리를 추가했다면 Cards 탭의 Prompt Templates Editor에서
- category_seeds에 시드 키워드도 함께 정의해야 자동 추출에 반영됩니다.
-
-
- );
-}
-
-/* ══════════════════════ Trends 탭 패널 2: ExternalTrendsPanel ══════════ */
-const CATEGORY_COLORS = {
- economy: '#0F62FE', psychology: '#A66CFF',
- celebrity: '#FF5C8A', uncategorized: '#6B7280',
-};
-
-function ExternalTrendsPanel({ onCreateSlate }) {
- const [naver, setNaver] = useState([]);
- const [google, setGoogle] = useState([]);
- const [lastFetched, setLastFetched] = useState(null);
- const [collecting, setCollecting] = useState(false);
- const [task, setTask] = useState(null);
-
- const load = useCallback(async () => {
- const [n, g] = await Promise.all([
- getInstaTrends({ source: 'naver_popular', days: 2 }),
- getInstaTrends({ source: 'youtube_trending', days: 2 }),
- ]);
- setNaver(n.items || []);
- setGoogle(g.items || []);
- const all = [...(n.items || []), ...(g.items || [])];
- if (all.length) {
- const latest = all.map(t => t.suggested_at).sort().reverse()[0];
- setLastFetched(latest);
- }
- }, []);
-
- useEffect(() => { load(); }, [load]);
-
- const trigger = async () => {
- setCollecting(true);
- try {
- const { task_id } = await instaCollectTrends();
- let st = null;
- for (let i = 0; i < 60; i++) {
- st = await getInstaTask(task_id);
- setTask(st);
- if (st.status === 'succeeded' || st.status === 'failed') break;
- await new Promise(r => setTimeout(r, 3000));
- }
- await load();
- } finally { setCollecting(false); }
- };
-
- const groupByCat = (items) => {
- const g = {};
- items.forEach(it => { (g[it.category] = g[it.category] || []).push(it); });
- return g;
- };
-
- const renderRow = (t) => (
-
-
- {t.keyword}
- {(t.score || 0).toFixed(2)}
-
-
- );
-
- const naverGrouped = groupByCat(naver);
- return (
-
-
-
📈 외부 트렌드
-
-
- {lastFetched ? `마지막 수집: ${fmtDate(lastFetched)}` : '아직 수집 없음'}
-
-
-
-
- {task && }
-
-
-
🔥 NAVER 인기
- {Object.keys(naverGrouped).length === 0 &&
없음
}
- {Object.entries(naverGrouped).map(([cat, items]) => (
-
-
{cat}
- {items.map(renderRow)}
-
- ))}
-
-
-
📺 YouTube 인기
- {google.length === 0 &&
없음
}
- {google.map(renderRow)}
-
-
-
- );
-}
-
-/* ══════════════════════ Trends 탭 패널 3: PreferenceImpactPanel ══════ */
-function PreferenceImpactPanel() {
- const [prefs, setPrefs] = useState([]);
- const TOTAL = 15;
-
- useEffect(() => {
- (async () => {
- const data = await getInstaPreferences();
- setPrefs(data.categories || []);
- })();
- }, []);
-
- const totalWeight = prefs.reduce((s, p) => s + (p.weight || 0), 0) || 1;
- const breakdown = prefs.map(p => ({
- category: p.category,
- count: Math.round(TOTAL * (p.weight || 0) / totalWeight),
- }));
-
- return (
-
- 📊 다음 자동 추출 미리보기
-
- {breakdown.map(b => (
-
- {b.category}
- {b.count}개
-
- ))}
-
-
- );
-}
+import PromptTemplatesEditor from './components/PromptTemplatesEditor';
+import AccountFocusPanel from './components/AccountFocusPanel';
+import ExternalTrendsPanel from './components/ExternalTrendsPanel';
+import PreferenceImpactPanel from './components/PreferenceImpactPanel';
/* ══════════════════════════════════════════════════════════════════════════ */
export default function InstaCards() {
@@ -387,78 +183,3 @@ export default function InstaCards() {
);
}
-
-/* ══════════════════════ 프롬프트 템플릿 에디터 ══════════════════════════ */
-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}
-
- )}
-
- ))}
-
- );
-}
diff --git a/src/pages/insta/components/AccountFocusPanel.jsx b/src/pages/insta/components/AccountFocusPanel.jsx
new file mode 100644
index 0000000..8205343
--- /dev/null
+++ b/src/pages/insta/components/AccountFocusPanel.jsx
@@ -0,0 +1,73 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import { getInstaPreferences, putInstaPreferences } from '../../../api';
+
+/* ══════════════════════ Trends 탭 패널 1: AccountFocusPanel ══════════════ */
+function AccountFocusPanel() {
+ const [prefs, setPrefs] = useState([]);
+ const [draft, setDraft] = useState({});
+ const [saving, setSaving] = useState(false);
+ const [newCat, setNewCat] = useState('');
+
+ const load = useCallback(async () => {
+ const data = await getInstaPreferences();
+ setPrefs(data.categories || []);
+ const m = {};
+ (data.categories || []).forEach(p => { m[p.category] = Math.round(p.weight * 100); });
+ setDraft(m);
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ const save = async () => {
+ setSaving(true);
+ try {
+ const payload = {};
+ Object.entries(draft).forEach(([k, v]) => { payload[k] = (Number(v) || 0) / 100; });
+ await putInstaPreferences(payload);
+ await load();
+ } finally { setSaving(false); }
+ };
+
+ const addCat = () => {
+ const name = newCat.trim().toLowerCase();
+ if (!name || draft[name] !== undefined) return;
+ setDraft({ ...draft, [name]: 0 });
+ setNewCat('');
+ };
+
+ return (
+
+ 🎯 이 계정의 주제 (카테고리 가중치)
+ 슬라이더는 각 카테고리에 자동 추출 키워드 비율을 결정합니다. 합계는 자동 정규화됩니다.
+
+ {Object.entries(draft).map(([cat, val]) => (
+
+
+ setDraft({ ...draft, [cat]: Number(e.target.value) })}
+ className="ic-focus__slider"
+ />
+ {val}%
+
+ ))}
+
+
+ setNewCat(e.target.value)}
+ />
+
+
+
+
+ 💡 신규 카테고리를 추가했다면 Cards 탭의 Prompt Templates Editor에서
+ category_seeds에 시드 키워드도 함께 정의해야 자동 추출에 반영됩니다.
+
+
+ );
+}
+
+export default AccountFocusPanel;
diff --git a/src/pages/insta/components/ExternalTrendsPanel.jsx b/src/pages/insta/components/ExternalTrendsPanel.jsx
new file mode 100644
index 0000000..dfcfbf9
--- /dev/null
+++ b/src/pages/insta/components/ExternalTrendsPanel.jsx
@@ -0,0 +1,104 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import TaskStatusBox from './TaskStatusBox';
+import { fmtDate } from '../instaUtils';
+import { getInstaTrends, instaCollectTrends, getInstaTask } from '../../../api';
+
+/* ══════════════════════ Trends 탭 패널 2: ExternalTrendsPanel ══════════ */
+const CATEGORY_COLORS = {
+ economy: '#0F62FE', psychology: '#A66CFF',
+ celebrity: '#FF5C8A', uncategorized: '#6B7280',
+};
+
+function ExternalTrendsPanel({ onCreateSlate }) {
+ const [naver, setNaver] = useState([]);
+ const [google, setGoogle] = useState([]);
+ const [lastFetched, setLastFetched] = useState(null);
+ const [collecting, setCollecting] = useState(false);
+ const [task, setTask] = useState(null);
+
+ const load = useCallback(async () => {
+ const [n, g] = await Promise.all([
+ getInstaTrends({ source: 'naver_popular', days: 2 }),
+ getInstaTrends({ source: 'youtube_trending', days: 2 }),
+ ]);
+ setNaver(n.items || []);
+ setGoogle(g.items || []);
+ const all = [...(n.items || []), ...(g.items || [])];
+ if (all.length) {
+ const latest = all.map(t => t.suggested_at).sort().reverse()[0];
+ setLastFetched(latest);
+ }
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ const trigger = async () => {
+ setCollecting(true);
+ try {
+ const { task_id } = await instaCollectTrends();
+ let st = null;
+ for (let i = 0; i < 60; i++) {
+ st = await getInstaTask(task_id);
+ setTask(st);
+ if (st.status === 'succeeded' || st.status === 'failed') break;
+ await new Promise(r => setTimeout(r, 3000));
+ }
+ await load();
+ } finally { setCollecting(false); }
+ };
+
+ const groupByCat = (items) => {
+ const g = {};
+ items.forEach(it => { (g[it.category] = g[it.category] || []).push(it); });
+ return g;
+ };
+
+ const renderRow = (t) => (
+
+
+ {t.keyword}
+ {(t.score || 0).toFixed(2)}
+
+
+ );
+
+ const naverGrouped = groupByCat(naver);
+ return (
+
+
+
📈 외부 트렌드
+
+
+ {lastFetched ? `마지막 수집: ${fmtDate(lastFetched)}` : '아직 수집 없음'}
+
+
+
+
+ {task && }
+
+
+
🔥 NAVER 인기
+ {Object.keys(naverGrouped).length === 0 &&
없음
}
+ {Object.entries(naverGrouped).map(([cat, items]) => (
+
+
{cat}
+ {items.map(renderRow)}
+
+ ))}
+
+
+
📺 YouTube 인기
+ {google.length === 0 &&
없음
}
+ {google.map(renderRow)}
+
+
+
+ );
+}
+
+export default ExternalTrendsPanel;
diff --git a/src/pages/insta/components/PreferenceImpactPanel.jsx b/src/pages/insta/components/PreferenceImpactPanel.jsx
new file mode 100644
index 0000000..0ba3463
--- /dev/null
+++ b/src/pages/insta/components/PreferenceImpactPanel.jsx
@@ -0,0 +1,37 @@
+import React, { useState, useEffect } from 'react';
+import { getInstaPreferences } from '../../../api';
+
+/* ══════════════════════ Trends 탭 패널 3: PreferenceImpactPanel ══════ */
+function PreferenceImpactPanel() {
+ const [prefs, setPrefs] = useState([]);
+ const TOTAL = 15;
+
+ useEffect(() => {
+ (async () => {
+ const data = await getInstaPreferences();
+ setPrefs(data.categories || []);
+ })();
+ }, []);
+
+ const totalWeight = prefs.reduce((s, p) => s + (p.weight || 0), 0) || 1;
+ const breakdown = prefs.map(p => ({
+ category: p.category,
+ count: Math.round(TOTAL * (p.weight || 0) / totalWeight),
+ }));
+
+ return (
+
+ 📊 다음 자동 추출 미리보기
+
+ {breakdown.map(b => (
+
+ {b.category}
+ {b.count}개
+
+ ))}
+
+
+ );
+}
+
+export default PreferenceImpactPanel;
diff --git a/src/pages/insta/components/PromptTemplatesEditor.jsx b/src/pages/insta/components/PromptTemplatesEditor.jsx
new file mode 100644
index 0000000..92db623
--- /dev/null
+++ b/src/pages/insta/components/PromptTemplatesEditor.jsx
@@ -0,0 +1,80 @@
+import React, { useState, useEffect } from 'react';
+import { fmtDate } from '../instaUtils';
+import { getInstaPrompt, putInstaPrompt } from '../../../api';
+
+/* ══════════════════════ 프롬프트 템플릿 에디터 ══════════════════════════ */
+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}
+
+ )}
+
+ ))}
+
+ );
+}
+
+export default PromptTemplatesEditor;