feat(insta): 카드 탭 트렌딩 키워드 중복 제거 + 10개씩 페이지네이션

KeywordsPanel이 전체 목록을 세로로 길게 표시하던 것을, 동일 keyword
중복 제거(최고 score 유지)·score 내림차순 후 페이지당 10개만 렌더하고
이전(←)/다음(→) 페이저로 탐색하도록 변경. 카테고리 변경 시 첫 페이지 리셋.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 03:03:36 +09:00
parent a846ab89e6
commit c998753eea
2 changed files with 69 additions and 18 deletions

View File

@@ -59,6 +59,18 @@
.ic-keyword-row__meta { font-size: 0.72rem; color: rgba(255,255,255,.35); white-space: nowrap; } .ic-keyword-row__meta { font-size: 0.72rem; color: rgba(255,255,255,.35); white-space: nowrap; }
.ic-keyword-row__score { font-size: 0.75rem; font-weight: 700; color: #ec4899; min-width: 36px; text-align: right; } .ic-keyword-row__score { font-size: 0.75rem; font-weight: 700; color: #ec4899; min-width: 36px; text-align: right; }
/* 키워드 페이저 (10개씩, 이전/다음) */
.ic-keywords__pager { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 12px; }
.ic-pager-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: 99px;
border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.04);
color: rgba(255,255,255,.7); font-size: 1.1rem; cursor: pointer; transition: all .15s;
}
.ic-pager-btn:hover:not(:disabled) { background: rgba(236,72,153,.18); border-color: #ec4899; color: #ec4899; }
.ic-pager-btn:disabled { opacity: .3; cursor: not-allowed; }
.ic-pager-info { font-size: 0.8rem; font-weight: 600; color: rgba(255,255,255,.55); min-width: 48px; text-align: center; }
/* 슬레이트 그리드 — 모바일 2칸 강제, 데스크탑 auto-fill */ /* 슬레이트 그리드 — 모바일 2칸 강제, 데스크탑 auto-fill */
.ic-slates-grid { .ic-slates-grid {
display: grid; display: grid;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import PullToRefresh from '../../components/PullToRefresh'; import PullToRefresh from '../../components/PullToRefresh';
import { import {
getInstaStatus, getInstaStatus,
@@ -521,11 +521,13 @@ function TriggerPanel() {
/* ══════════════════════ 키워드 목록 ══════════════════════════════════════ */ /* ══════════════════════ 키워드 목록 ══════════════════════════════════════ */
const CATEGORIES = ['전체', 'economy', 'psychology', 'celebrity']; const CATEGORIES = ['전체', 'economy', 'psychology', 'celebrity'];
const KEYWORDS_PER_PAGE = 10;
function KeywordsPanel({ onCreateSlate }) { function KeywordsPanel({ onCreateSlate }) {
const [category, setCategory] = useState('전체'); const [category, setCategory] = useState('전체');
const [keywords, setKeywords] = useState([]); const [keywords, setKeywords] = useState([]);
const [creating, setCreating] = useState(null); // keyword_id being created const [creating, setCreating] = useState(null); // keyword_id being created
const [page, setPage] = useState(0);
const load = useCallback(() => { const load = useCallback(() => {
const cat = category === '전체' ? undefined : category; const cat = category === '전체' ? undefined : category;
@@ -533,6 +535,23 @@ function KeywordsPanel({ onCreateSlate }) {
}, [category]); }, [category]);
useEffect(() => { load(); }, [load]); useEffect(() => { load(); }, [load]);
useEffect(() => { setPage(0); }, [category]); // 카테고리 변경 시 첫 페이지로
// 동일 keyword 중복 제거(최고 score 1개만 유지) + score 내림차순
const deduped = useMemo(() => {
const best = new Map();
for (const kw of keywords) {
const name = (kw.keyword || '').trim();
if (!name) continue;
const prev = best.get(name);
if (!prev || (kw.score ?? 0) > (prev.score ?? 0)) best.set(name, kw);
}
return [...best.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
}, [keywords]);
const totalPages = Math.max(1, Math.ceil(deduped.length / KEYWORDS_PER_PAGE));
const safePage = Math.min(page, totalPages - 1);
const pageItems = deduped.slice(safePage * KEYWORDS_PER_PAGE, safePage * KEYWORDS_PER_PAGE + KEYWORDS_PER_PAGE);
// 부모(InstaCards)의 handleCreateSlate에 위임 — progress 배너 + 스크롤 + 자동 미리보기 공통화 // 부모(InstaCards)의 handleCreateSlate에 위임 — progress 배너 + 스크롤 + 자동 미리보기 공통화
async function handleCreate(kw) { async function handleCreate(kw) {
@@ -568,11 +587,12 @@ function KeywordsPanel({ onCreateSlate }) {
{/* progress 표시는 상단 ic-slate-progress 배너에서 일괄 처리 */} {/* progress 표시는 상단 ic-slate-progress 배너에서 일괄 처리 */}
{keywords.length === 0 ? ( {deduped.length === 0 ? (
<div className="ic-empty">키워드가 없습니다. 키워드 추출을 실행하세요.</div> <div className="ic-empty">키워드가 없습니다. 키워드 추출을 실행하세요.</div>
) : ( ) : (
<>
<div className="ic-keywords"> <div className="ic-keywords">
{keywords.map((kw) => ( {pageItems.map((kw) => (
<div key={kw.id} className="ic-keyword-row"> <div key={kw.id} className="ic-keyword-row">
<span className="ic-keyword-row__kw">{kw.keyword}</span> <span className="ic-keyword-row__kw">{kw.keyword}</span>
<span className="ic-keyword-row__meta"> <span className="ic-keyword-row__meta">
@@ -589,6 +609,25 @@ function KeywordsPanel({ onCreateSlate }) {
</div> </div>
))} ))}
</div> </div>
{totalPages > 1 && (
<div className="ic-keywords__pager">
<button
className="ic-pager-btn"
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={safePage === 0}
aria-label="이전 키워드"
></button>
<span className="ic-pager-info">{safePage + 1} / {totalPages}</span>
<button
className="ic-pager-btn"
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={safePage >= totalPages - 1}
aria-label="다음 키워드"
></button>
</div>
)}
</>
)} )}
</div> </div>
); );