import { useState, useCallback, useEffect } from "react"; import { toast } from "sonner"; import PageHeader from "@/components/common/PageHeader"; import SearchInput from "@/components/common/SearchInput"; import FilterDropdown from "@/components/common/FilterDropdown"; import FilterResetButton from "@/components/common/FilterResetButton"; import Pagination from "@/components/common/Pagination"; import EmptyState from "@/components/common/EmptyState"; import PlatformBadge from "@/components/common/PlatformBadge"; import SecretToggleCell from "../components/SecretToggleCell"; import DeviceSlidePanel from "../components/DeviceSlidePanel"; import { formatDate } from "@/utils/format"; import { fetchDevices, exportDevices } from "@/api/device.api"; import { fetchServices } from "@/api/service.api"; import { PLATFORM_FILTER_OPTIONS, PUSH_CONSENT_FILTER_OPTIONS, } from "../types"; import type { DeviceListItem } from "../types"; const PAGE_SIZE = 10; // 테이블 컬럼 헤더 const COLUMNS = [ "소속 서비스", "플랫폼", "Device ID", "Push Token", "푸시 수신", "광고 수신", "태그", "등록일", ]; export default function DeviceListPage() { // 필터 입력 상태 const [search, setSearch] = useState(""); const [serviceFilter, setServiceFilter] = useState("전체 서비스"); const [platformFilter, setPlatformFilter] = useState("전체"); const [pushFilter, setPushFilter] = useState("전체"); const [currentPage, setCurrentPage] = useState(1); const [loading, setLoading] = useState(false); // 적용된 필터 const [appliedSearch, setAppliedSearch] = useState(""); const [appliedService, setAppliedService] = useState("전체 서비스"); const [appliedPlatform, setAppliedPlatform] = useState("전체"); const [appliedPush, setAppliedPush] = useState("전체"); // 서비스 목록 (API에서 로드) const [serviceFilterOptions, setServiceFilterOptions] = useState([ "전체 서비스", ]); const [serviceCodeMap, setServiceCodeMap] = useState< Record >({}); // 데이터 const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); const [totalPages, setTotalPages] = useState(1); // 슬라이드 패널 const [panelOpen, setPanelOpen] = useState(false); const [selectedDevice, setSelectedDevice] = useState( null, ); // SecretToggleCell 배타적 관리 const [openDropdownKey, setOpenDropdownKey] = useState(null); // 서비스 목록 로드 (초기화 시 1회) useEffect(() => { (async () => { try { const res = await fetchServices({ page: 1, pageSize: 100 }); const svcItems = res.data.data.items ?? []; const names = svcItems.map((s) => s.serviceName); setServiceFilterOptions(["전체 서비스", ...names]); const codeMap: Record = {}; svcItems.forEach((s) => { codeMap[s.serviceName] = s.serviceCode; }); setServiceCodeMap(codeMap); } catch { // 서비스 목록 로드 실패 시 기본값 유지 } })(); }, []); // 데이터 로드 const loadData = useCallback( async ( page: number, keyword: string, serviceName: string, platform: string, push: string, ) => { setLoading(true); try { const serviceCode = serviceName !== "전체 서비스" ? serviceCodeMap[serviceName] || undefined : undefined; const res = await fetchDevices( { page, size: PAGE_SIZE, keyword: keyword || undefined, platform: platform !== "전체" ? platform : undefined, push_agreed: push === "동의" ? true : push === "미동의" ? false : undefined, }, serviceCode, ); const data = res.data.data; setItems(data.items ?? []); setTotalItems(data.totalCount ?? 0); setTotalPages(data.totalPages ?? 1); } catch { setItems([]); setTotalItems(0); setTotalPages(1); } finally { setLoading(false); } }, [serviceCodeMap], ); // 초기 로드 useEffect(() => { loadData(1, "", "전체 서비스", "전체", "전체"); }, [loadData]); // 조회 const handleQuery = () => { setAppliedSearch(search); setAppliedService(serviceFilter); setAppliedPlatform(platformFilter); setAppliedPush(pushFilter); setCurrentPage(1); loadData(1, search, serviceFilter, platformFilter, pushFilter); }; // 필터 초기화 const handleReset = () => { setSearch(""); setServiceFilter("전체 서비스"); setPlatformFilter("전체"); setPushFilter("전체"); setAppliedSearch(""); setAppliedService("전체 서비스"); setAppliedPlatform("전체"); setAppliedPush("전체"); setCurrentPage(1); loadData(1, "", "전체 서비스", "전체", "전체"); }; // 페이지 변경 const handlePageChange = (page: number) => { setCurrentPage(page); loadData(page, appliedSearch, appliedService, appliedPlatform, appliedPush); }; // 행 클릭 const handleRowClick = (device: DeviceListItem) => { setOpenDropdownKey(null); setSelectedDevice(device); setPanelOpen(true); }; // 패널 닫기 (삭제 후 목록 새로고침) const handlePanelClose = () => { setPanelOpen(false); loadData( currentPage, appliedSearch, appliedService, appliedPlatform, appliedPush, ); }; // 엑셀 내보내기 const handleExport = async () => { if (appliedService === "전체 서비스") { toast.error("엑셀 다운로드를 위해 서비스를 선택해주세요."); return; } const serviceCode = serviceCodeMap[appliedService]; if (!serviceCode) return; try { const res = await exportDevices( { keyword: appliedSearch || undefined, platform: appliedPlatform !== "전체" ? appliedPlatform : undefined, push_agreed: appliedPush === "동의" ? true : appliedPush === "미동의" ? false : undefined, }, serviceCode, ); const blob = new Blob([res.data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `devices_${new Date().toISOString().split("T")[0]}.xlsx`; a.click(); window.URL.revokeObjectURL(url); } catch { toast.error("엑셀 다운로드에 실패했습니다."); } }; // 체크/취소 아이콘 const StatusIcon = ({ active }: { active: boolean }) => ( {active ? "check_circle" : "cancel"} ); // 태그 아이콘 (있으면 체크, 없으면 dash) const TagIcon = ({ hasTags }: { hasTags: boolean }) => hasTags ? ( check_circle ) : ( cancel ); // 테이블 헤더 렌더링 const renderTableHead = () => ( {COLUMNS.map((col) => ( {col} ))} ); return (
download 엑셀 다운로드 } /> {/* 필터바 */}
{/* 테이블 */} {loading ? ( /* 로딩 스켈레톤 */
{renderTableHead()} {Array.from({ length: 5 }).map((_, i) => ( ))}
) : items.length > 0 ? (
{renderTableHead()} {items.map((device, idx) => ( handleRowClick(device)} > {/* 소속 서비스 */} {/* 플랫폼 */} {/* Device ID */} {/* Push Token */} {/* 푸시 수신 */} {/* 광고 수신 */} {/* 태그 */} {/* 등록일 */} ))}
{device.service_name} 0} /> {formatDate(device.created_at ?? "")}
{/* 페이지네이션 */}
) : ( )} {/* 슬라이드 패널 */}
); }