fromis_9/frontend/src/components/pc/admin/common/ConfirmDialog.jsx
caadiq ad8406fdd7 feat: 세트리스트 섹션 및 곡 검색 다이얼로그 추가
- 세트리스트 섹션: 곡명, 앨범명, 참여 멤버 선택
- 곡 검색 다이얼로그: 앨범별 트랙 검색 및 다중 선택
- 직접 입력과 곡 검색 두 가지 방식으로 곡 추가 가능
- 공연 일정/세트리스트 추가 버튼을 하단으로 이동
- ConfirmDialog에 createPortal 적용
- 콘서트 정보 → 공연 정보로 명칭 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 23:05:15 +09:00

117 lines
3.6 KiB
JavaScript

/**
* ConfirmDialog 컴포넌트
* 삭제 등 위험한 작업의 확인을 위한 공통 다이얼로그
*
* Props:
* - isOpen: 다이얼로그 표시 여부
* - onClose: 닫기 콜백
* - onConfirm: 확인 콜백
* - title: 제목 (예: "앨범 삭제")
* - message: 메시지 내용 (ReactNode 가능)
* - confirmText: 확인 버튼 텍스트 (기본: "삭제")
* - cancelText: 취소 버튼 텍스트 (기본: "취소")
* - loading: 로딩 상태
* - loadingText: 로딩 중 텍스트 (기본: "삭제 중...")
* - variant: 버튼 색상 (기본: "danger", "primary" 가능)
*/
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, Trash2 } from 'lucide-react';
function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = '삭제',
cancelText = '취소',
loading = false,
loadingText = '삭제 중...',
variant = 'danger',
icon: Icon = AlertTriangle,
}) {
// 버튼 색상 설정
const buttonColors = {
danger: 'bg-red-500 hover:bg-red-600',
primary: 'bg-primary hover:bg-primary-dark',
};
const iconBgColors = {
danger: 'bg-red-100',
primary: 'bg-primary/10',
};
const iconColors = {
danger: 'text-red-500',
primary: 'text-primary',
};
return createPortal(
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={() => !loading && onClose()}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="flex items-center gap-3 mb-4">
<div
className={`w-10 h-10 rounded-full ${iconBgColors[variant]} flex items-center justify-center`}
>
<Icon className={iconColors[variant]} size={20} />
</div>
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
</div>
{/* 메시지 */}
<div className="text-gray-600 mb-6">{message}</div>
{/* 버튼 */}
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onClose}
disabled={loading}
className="px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
>
{cancelText}
</button>
<button
type="button"
onClick={onConfirm}
disabled={loading}
className={`px-4 py-2 ${buttonColors[variant]} text-white rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50`}
>
{loading ? (
<>
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{loadingText}
</>
) : (
<>
<Trash2 size={16} />
{confirmText}
</>
)}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body
);
}
export default ConfirmDialog;