import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, CheckCircle2, Database, ShieldAlert, } from "lucide-react"; import { useState } from "react"; import { RoleGuard } from "../../components/auth/RoleGuard"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { type DataIntegrityCheck, type DataIntegrityStatus, type OrphanUserLoginID, deleteOrphanUserLoginIDs, fetchDataIntegrityReport, fetchOrphanUserLoginIDs, } from "../../lib/adminApi"; function statusLabel(status: DataIntegrityStatus) { switch (status) { case "pass": return "정상"; case "warning": return "주의"; case "fail": return "실패"; default: return status; } } function statusBadgeVariant(status: DataIntegrityStatus) { switch (status) { case "pass": return "success"; case "warning": return "warning"; default: return "warning"; } } function formatDateTime(value?: string) { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("ko-KR", { dateStyle: "medium", timeStyle: "medium", }).format(date); } function CheckIcon({ check }: { check: DataIntegrityCheck }) { if (check.status === "pass") { return ; } if (check.status === "warning") { return ; } return ; } function reasonLabel(reason: string) { switch (reason) { case "missing_user": return "사용자 없음"; case "deleted_user": return "삭제된 사용자"; case "missing_tenant": return "테넌트 없음"; case "deleted_tenant": return "삭제된 테넌트"; default: return reason; } } function recheckStatusText(status: "idle" | "running" | "success" | "error") { switch (status) { case "running": return "정합성 검사를 실행 중입니다."; case "success": return "검사가 완료되었습니다."; case "error": return "검사에 실패했습니다."; default: return ""; } } function OrphanLoginIDTable({ items, selectedIds, onToggle, }: { items: OrphanUserLoginID[]; selectedIds: string[]; onToggle: (id: string) => void; }) { if (items.length === 0) { return (
삭제할 유령 로그인 ID가 없습니다.
); } const selectedSet = new Set(selectedIds); return (
{items.map((item) => ( ))}
선택 Login ID Field User Tenant 사유
onToggle(item.id)} className="h-4 w-4 rounded border-input" /> {item.loginId} {item.fieldKey}
{item.userEmail || "-"}
{item.userId}
{item.tenantSlug || "-"}
{item.tenantId}
{item.reasons.map((reason) => ( {reasonLabel(reason)} ))}
); } function DataIntegrityContent() { const queryClient = useQueryClient(); const [selectedOrphanIds, setSelectedOrphanIds] = useState([]); const [recheckStatus, setRecheckStatus] = useState< "idle" | "running" | "success" | "error" >("idle"); const { data, isLoading, isError, error, refetch, isFetching } = useQuery({ queryKey: ["data-integrity-report"], queryFn: fetchDataIntegrityReport, }); const orphanLoginIDsQuery = useQuery({ queryKey: ["orphan-user-login-ids"], queryFn: fetchOrphanUserLoginIDs, }); const deleteMutation = useMutation({ mutationFn: deleteOrphanUserLoginIDs, onSuccess: async () => { setSelectedOrphanIds([]); await Promise.all([ queryClient.invalidateQueries({ queryKey: ["data-integrity-report"] }), queryClient.invalidateQueries({ queryKey: ["orphan-user-login-ids"] }), ]); }, }); const orphanItems = orphanLoginIDsQuery.data?.items ?? []; const toggleOrphanID = (id: string) => { setSelectedOrphanIds((current) => current.includes(id) ? current.filter((selectedID) => selectedID !== id) : [...current, id], ); }; const handleDeleteSelected = () => { if (selectedOrphanIds.length === 0) { return; } const confirmed = window.confirm( `선택한 ${selectedOrphanIds.length}개의 유령 로그인 ID를 삭제하시겠습니까?`, ); if (confirmed) { deleteMutation.mutate(selectedOrphanIds); } }; const isManualRechecking = recheckStatus === "running"; const handleRecheck = async () => { if (isManualRechecking) { return; } setRecheckStatus("running"); const result = await refetch(); setRecheckStatus(result.isError ? "error" : "success"); }; const recheckMessage = recheckStatusText(recheckStatus); return (

System

데이터 정합성 검증

{recheckMessage ? ( {recheckMessage} ) : null}
{isError ? (
{(error as Error)?.message || "정합성 리포트를 불러오지 못했습니다."}
) : null}

Read model integrity

Ory SoT를 덮어쓰지 않고 backend DB read model의 이상 징후만 확인합니다.

{data ? ( {statusLabel(data.status)} ) : null}
{isLoading ? (
불러오는 중
) : (
검사 항목
{data?.summary.totalChecks ?? 0}
정상
{data?.summary.passed ?? 0}
실패 건수
{data?.summary.failures ?? 0}
검사 시각
{formatDateTime(data?.checkedAt)}
)}
{(data?.sections ?? []).map((section) => (

{section.label}

{statusLabel(section.status)}
{section.checks.map((check) => (
{check.label}

{check.description}

{statusLabel(check.status)} {check.count}
))}
))}

유령 로그인 ID 정리

삭제되었거나 존재하지 않는 사용자/테넌트를 참조하는 로그인 ID를 확인한 뒤 선택 삭제합니다.

{orphanLoginIDsQuery.isError ? (
유령 로그인 ID 대상을 불러오지 못했습니다.
) : null} {deleteMutation.data ? (
{deleteMutation.data.deletedCount}개의 유령 로그인 ID를 삭제했습니다.
) : null}
); } export default function DataIntegrityPage() { return (

접근 권한이 없습니다

이 화면은 super_admin 권한으로만 접근할 수 있습니다.

} >
); }