125 lines
4.4 KiB
React
125 lines
4.4 KiB
React
|
|
/**
|
||
|
|
* 로그 상세 다이얼로그
|
||
|
|
*/
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|