"use client"; import { useCallback, useEffect, useState } from "react"; import { api } from "@/lib/api"; import type { Channel } from "@/lib/api"; // #329 — admin/page.tsx에서 추출 (내부 로직 그대로) export function ChannelsPanel({ isAdmin }: { isAdmin: boolean }) { const [channels, setChannels] = useState([]); const [newId, setNewId] = useState(""); const [newName, setNewName] = useState(""); const [newFilter, setNewFilter] = useState(""); const [loading, setLoading] = useState(false); const [scanResult, setScanResult] = useState>({}); const load = useCallback(() => { api.getChannels().then(setChannels).catch(console.error); }, []); useEffect(() => { load(); }, [load]); const handleAdd = async () => { if (!newId.trim() || !newName.trim()) return; setLoading(true); try { await api.addChannel(newId.trim(), newName.trim(), newFilter.trim() || undefined); setNewId(""); setNewName(""); setNewFilter(""); load(); } catch (e: unknown) { alert(e instanceof Error ? e.message : "채널 추가 실패"); } finally { setLoading(false); } }; const [editingChannel, setEditingChannel] = useState(null); const [editDesc, setEditDesc] = useState(""); const [editTags, setEditTags] = useState(""); const [editOrder, setEditOrder] = useState(99); const handleSaveChannel = async (id: string) => { try { await api.updateChannel(id, { description: editDesc, tags: editTags, sort_order: editOrder }); setEditingChannel(null); load(); } catch { alert("채널 수정 실패"); } }; const handleDelete = async (channelId: string, channelName: string) => { if (!confirm(`"${channelName}" 채널을 삭제하시겠습니까?`)) return; try { await api.deleteChannel(channelId); load(); } catch { alert("채널 삭제 실패"); } }; const handleScan = async (channelId: string, full: boolean = false) => { setScanResult((prev) => ({ ...prev, [channelId]: full ? "전체 스캔 중..." : "스캔 중..." })); try { const res = await api.scanChannel(channelId, full); setScanResult((prev) => ({ ...prev, [channelId]: `${res.total_fetched}개 조회, ${res.new_videos}개 신규${(res as Record).filtered ? `, ${(res as Record).filtered}개 필터링` : ""}`, })); } catch { setScanResult((prev) => ({ ...prev, [channelId]: "스캔 실패" })); } }; return (
{isAdmin &&

채널 추가

setNewId(e.target.value)} className="border rounded px-3 py-2 flex-1 text-sm bg-surface text-gray-900" /> setNewName(e.target.value)} className="border rounded px-3 py-2 flex-1 text-sm bg-surface text-gray-900" /> setNewFilter(e.target.value)} className="border rounded px-3 py-2 w-40 text-sm bg-surface text-gray-900" />
}
{isAdmin && } {channels.map((ch) => ( {isAdmin && } ))} {channels.length === 0 && ( )}
채널 이름 Channel ID 제목 필터 설명 태그 순서 영상 수액션스캔 결과
{ch.channel_name} {ch.channel_id} {ch.title_filter ? ( {ch.title_filter} ) : ( 전체 )} {editingChannel === ch.id ? ( setEditDesc(e.target.value)} className="border rounded px-2 py-1 text-xs w-32 bg-surface text-gray-900" placeholder="설명" /> ) : ( { if (!isAdmin) return; setEditingChannel(ch.id); setEditDesc(ch.description || ""); setEditTags(ch.tags || ""); setEditOrder(ch.sort_order ?? 99); }}>{ch.description || -} )} {editingChannel === ch.id ? (
setEditTags(e.target.value)} className="border rounded px-2 py-1 text-xs w-40 bg-surface text-gray-900" placeholder="태그 (쉼표 구분)" />
) : ( { if (!isAdmin) return; setEditingChannel(ch.id); setEditDesc(ch.description || ""); setEditTags(ch.tags || ""); setEditOrder(ch.sort_order ?? 99); }}>{ch.tags ? ch.tags.split(",").map(t => t.trim()).join(", ") : -} )}
{editingChannel === ch.id ? ( setEditOrder(Number(e.target.value))} className="border rounded px-2 py-1 text-xs w-14 text-center bg-surface text-gray-900" min={1} /> ) : ( {ch.sort_order ?? 99} )} {ch.video_count > 0 ? ( {ch.video_count}개 ) : ( 0 )} {scanResult[ch.channel_id] || "-"}
등록된 채널이 없습니다
); } /* ─── 영상 관리 ─── */ type VideoSortKey = "status" | "channel_name" | "title" | "published_at";