"use client"; import { useCallback, useEffect, useState } from "react"; import { api } from "@/lib/api"; // #329 — admin/page.tsx에서 추출 interface AdminUser { id: string; email: string | null; nickname: string | null; avatar_url: string | null; is_admin: boolean; provider: string | null; created_at: string | null; favorite_count: number; review_count: number; memo_count: number; } interface UserFavorite { id: string; name: string; address: string | null; region: string | null; cuisine_type: string | null; rating: number | null; business_status: string | null; created_at: string | null; } interface UserReview { id: string; restaurant_id: string; rating: number; review_text: string | null; visited_at: string | null; created_at: string | null; restaurant_name: string | null; } interface UserMemo { id: string; restaurant_id: string; rating: number | null; memo_text: string | null; visited_at: string | null; created_at: string; restaurant_name: string | null; } export function UsersPanel() { const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [selectedUser, setSelectedUser] = useState(null); const [favorites, setFavorites] = useState([]); const [reviews, setReviews] = useState([]); const [memos, setMemos] = useState([]); const [detailLoading, setDetailLoading] = useState(false); const perPage = 20; const loadUsers = useCallback(async (p: number) => { try { const res = await api.getAdminUsers({ limit: perPage, offset: p * perPage }); setUsers(res.users); setTotal(res.total); } catch (e) { console.error("Failed to load users:", e); } }, []); useEffect(() => { loadUsers(page); }, [page, loadUsers]); const handleSelectUser = async (u: AdminUser) => { if (selectedUser?.id === u.id) { setSelectedUser(null); setFavorites([]); setReviews([]); setMemos([]); return; } setSelectedUser(u); setDetailLoading(true); try { const [favs, revs, mems] = await Promise.all([ api.getAdminUserFavorites(u.id), api.getAdminUserReviews(u.id), api.getAdminUserMemos(u.id), ]); setFavorites(favs); setReviews(revs); setMemos(mems); } catch (e) { console.error(e); } finally { setDetailLoading(false); } }; const totalPages = Math.ceil(total / perPage); return (

유저 관리 ({total}명)

{/* Users Table */}
{users.map((u) => ( handleSelectUser(u)} className={`border-t cursor-pointer transition-colors ${ selectedUser?.id === u.id ? "bg-brand-50" : "hover:bg-brand-50/50" }`} > ))}
사용자 이메일 관리자 리뷰 메모 가입일
{u.avatar_url ? ( ) : (
{(u.nickname || u.email || "?").charAt(0).toUpperCase()}
)} {u.nickname || "-"}
{u.email || "-"} {u.favorite_count > 0 ? ( {u.favorite_count} ) : ( 0 )} {u.review_count > 0 ? ( {u.review_count} ) : ( 0 )} {u.memo_count > 0 ? ( {u.memo_count} ) : ( 0 )} {u.created_at?.slice(0, 10) || "-"}
{/* Pagination */} {totalPages > 1 && (
{page + 1} / {totalPages}
)} {/* Selected User Detail */} {selectedUser && (
{selectedUser.avatar_url ? ( ) : (
{(selectedUser.nickname || selectedUser.email || "?").charAt(0).toUpperCase()}
)}
{selectedUser.nickname || "-"}
{selectedUser.email || "-"}
{selectedUser.provider} · 가입: {selectedUser.created_at?.slice(0, 10)}
{detailLoading ? (

로딩 중...

) : (
{/* Favorites */}

찜한 식당 ({favorites.length})

{favorites.length === 0 ? (

찜한 식당이 없습니다.

) : (
{favorites.map((f) => (
{f.name} {f.region && ( {f.region} )} {f.cuisine_type && ( · {f.cuisine_type} )} {f.business_status === "CLOSED_PERMANENTLY" && ( 폐업 )}
{f.rating && ( ★ {f.rating} )}
))}
)}
{/* Reviews */}

작성한 리뷰 ({reviews.length})

{reviews.length === 0 ? (

작성한 리뷰가 없습니다.

) : (
{reviews.map((r) => (
{r.restaurant_name || "알 수 없음"} {"★".repeat(Math.round(r.rating))} {r.rating}
{r.review_text && (

{r.review_text}

)}
{r.visited_at && `방문: ${r.visited_at} · `} {r.created_at?.slice(0, 10)}
))}
)}
{/* Memos */}

작성한 메모 ({memos.length})

{memos.length === 0 ? (

작성한 메모가 없습니다.

) : (
{memos.map((m) => (
{m.restaurant_name || "알 수 없음"} {m.rating && ( {"★".repeat(Math.round(m.rating))} {m.rating} )}
{m.memo_text && (

{m.memo_text}

)}
{m.visited_at && `방문: ${m.visited_at} · `} {m.created_at?.slice(0, 10)} 비공개
))}
)}
)}
)}
); } /* ─── 데몬 설정 ─── */