fromis_9/frontend/src/components/pc/admin/log/LogDetailDialog.jsx

125 lines
4.4 KiB
React
Raw Normal View History

/**
* 로그 상세 다이얼로그
*/
import { motion, AnimatePresence } from 'framer-motion';
import { X, User, Bot } from 'lucide-react';
import { ACTION_STYLES, ACTION_LABELS, CATEGORY_LABELS, parseSummary, formatDateTime, hasDetails } from './constants';
// 행위자 뱃지
function ActorBadge({ actor }) {
if (actor === 'admin') {
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-gray-100 text-gray-700 text-xs font-medium rounded-full">
<User size={12} />
관리자
</span>
);
}
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-indigo-50 text-indigo-700 text-xs font-medium rounded-full">
<Bot size={12} />
{actor}
</span>
);
}
// summary 렌더링
function Summary({ summary }) {
const { prefix, detail } = parseSummary(summary);
return (
<>
<span className="text-primary font-medium">[{prefix}]</span>
{detail && <span className="ml-1.5">{detail}</span>}
</>
);
}
export { ActorBadge, Summary };
export default function LogDetailDialog({ log, onClose }) {
return (
<AnimatePresence>
{log && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/40"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.15 }}
className="relative bg-white rounded-2xl shadow-xl max-w-lg w-full mx-4"
>
{/* 헤더 */}
<div className="flex items-center justify-between p-5 border-b border-gray-100">
<div className="flex items-center gap-3">
<span className={`inline-block px-2.5 py-1 text-xs font-medium rounded-full ${ACTION_STYLES[log.action] || 'bg-gray-100 text-gray-600'}`}>
{ACTION_LABELS[log.action] || log.action}
</span>
<span className="text-sm text-gray-500">
{CATEGORY_LABELS[log.category] || log.category}
</span>
</div>
<button
onClick={onClose}
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
>
<X size={18} className="text-gray-400" />
</button>
</div>
{/* 본문 */}
<div className="p-5 space-y-4">
{/* 내용 */}
<div>
<div className="text-sm text-gray-400 mb-1.5">내용</div>
<div className="text-sm text-gray-900 leading-relaxed">
<Summary summary={log.summary} />
</div>
</div>
{/* 행위자 + 시간 */}
<div className="flex gap-6">
<div>
<div className="text-sm text-gray-400 mb-1.5">행위자</div>
<ActorBadge actor={log.actor} />
</div>
<div>
<div className="text-sm text-gray-400 mb-1.5">시간</div>
<span className="text-sm text-gray-700 tabular-nums">{formatDateTime(log.created_at)}</span>
</div>
</div>
{/* 대상 */}
{(log.target_type || log.target_id) && (
<div>
<div className="text-sm text-gray-400 mb-1.5">대상</div>
<span className="text-sm text-gray-700">
{log.target_type && <span>{log.target_type}</span>}
{log.target_id && <span className="ml-1.5 text-gray-400">#{log.target_id}</span>}
</span>
</div>
)}
{/* 상세 정보 */}
{hasDetails(log.details) && (
<div>
<div className="text-sm text-gray-400 mb-1.5">상세 정보</div>
<pre className="text-xs text-gray-600 bg-gray-50 rounded-lg p-3 overflow-auto max-h-40">
{JSON.stringify(log.details, null, 2)}
</pre>
</div>
)}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}