- 메뉴: 즐겨찾기를 할일 좌측으로 이동 - 게시판: 답글 있는 게시글 삭제 방지 (댓글은 허용) - 즐겨찾기: ESC 키로 다이얼로그 닫기 지원 - 프로젝트: 기본 필터를 검토/진행/완료로 변경 (보류 해제)
202 lines
7.6 KiB
TypeScript
202 lines
7.6 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { X, Star, Folder, Globe, FileText, Database, Server, Mail, Image, Film, Music, Archive, Terminal, Settings, HardDrive, Network, Cloud } from 'lucide-react';
|
|
import { comms } from '@/communication';
|
|
import { FavoriteItem } from '@/types';
|
|
|
|
interface FavoriteDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
// URL 유형에 따른 아이콘 및 색상 반환
|
|
function getIconInfo(url: string): { icon: React.ElementType; color: string; bgColor: string } {
|
|
const lowerUrl = url.toLowerCase();
|
|
|
|
// 로컬 폴더/드라이브 경로
|
|
if (lowerUrl.match(/^[a-z]:\\/i) || lowerUrl.startsWith('\\\\') || lowerUrl.startsWith('file:')) {
|
|
// 네트워크 경로
|
|
if (lowerUrl.startsWith('\\\\')) {
|
|
return { icon: Network, color: 'text-purple-400', bgColor: 'from-purple-500/30 to-purple-600/30' };
|
|
}
|
|
return { icon: Folder, color: 'text-yellow-400', bgColor: 'from-yellow-500/30 to-yellow-600/30' };
|
|
}
|
|
|
|
// 웹 URL
|
|
if (lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://')) {
|
|
// 특정 서비스 감지
|
|
if (lowerUrl.includes('mail') || lowerUrl.includes('outlook') || lowerUrl.includes('gmail')) {
|
|
return { icon: Mail, color: 'text-red-400', bgColor: 'from-red-500/30 to-red-600/30' };
|
|
}
|
|
if (lowerUrl.includes('cloud') || lowerUrl.includes('drive') || lowerUrl.includes('onedrive') || lowerUrl.includes('dropbox')) {
|
|
return { icon: Cloud, color: 'text-sky-400', bgColor: 'from-sky-500/30 to-sky-600/30' };
|
|
}
|
|
if (lowerUrl.includes('server') || lowerUrl.includes('admin')) {
|
|
return { icon: Server, color: 'text-orange-400', bgColor: 'from-orange-500/30 to-orange-600/30' };
|
|
}
|
|
if (lowerUrl.includes('database') || lowerUrl.includes('sql') || lowerUrl.includes('db')) {
|
|
return { icon: Database, color: 'text-emerald-400', bgColor: 'from-emerald-500/30 to-emerald-600/30' };
|
|
}
|
|
return { icon: Globe, color: 'text-blue-400', bgColor: 'from-blue-500/30 to-blue-600/30' };
|
|
}
|
|
|
|
// 실행 파일
|
|
if (lowerUrl.endsWith('.exe') || lowerUrl.endsWith('.bat') || lowerUrl.endsWith('.cmd') || lowerUrl.endsWith('.ps1')) {
|
|
return { icon: Terminal, color: 'text-green-400', bgColor: 'from-green-500/30 to-green-600/30' };
|
|
}
|
|
|
|
// 문서 파일
|
|
if (lowerUrl.match(/\.(doc|docx|pdf|txt|xls|xlsx|ppt|pptx|hwp)$/i)) {
|
|
return { icon: FileText, color: 'text-blue-400', bgColor: 'from-blue-500/30 to-blue-600/30' };
|
|
}
|
|
|
|
// 이미지 파일
|
|
if (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) {
|
|
return { icon: Image, color: 'text-pink-400', bgColor: 'from-pink-500/30 to-pink-600/30' };
|
|
}
|
|
|
|
// 비디오 파일
|
|
if (lowerUrl.match(/\.(mp4|avi|mkv|mov|wmv)$/i)) {
|
|
return { icon: Film, color: 'text-violet-400', bgColor: 'from-violet-500/30 to-violet-600/30' };
|
|
}
|
|
|
|
// 오디오 파일
|
|
if (lowerUrl.match(/\.(mp3|wav|flac|aac|ogg)$/i)) {
|
|
return { icon: Music, color: 'text-rose-400', bgColor: 'from-rose-500/30 to-rose-600/30' };
|
|
}
|
|
|
|
// 압축 파일
|
|
if (lowerUrl.match(/\.(zip|rar|7z|tar|gz)$/i)) {
|
|
return { icon: Archive, color: 'text-amber-400', bgColor: 'from-amber-500/30 to-amber-600/30' };
|
|
}
|
|
|
|
// 설정 파일
|
|
if (lowerUrl.match(/\.(ini|cfg|config|xml|json|yaml|yml)$/i)) {
|
|
return { icon: Settings, color: 'text-gray-400', bgColor: 'from-gray-500/30 to-gray-600/30' };
|
|
}
|
|
|
|
// 드라이브 루트
|
|
if (lowerUrl.match(/^[a-z]:$/i)) {
|
|
return { icon: HardDrive, color: 'text-slate-400', bgColor: 'from-slate-500/30 to-slate-600/30' };
|
|
}
|
|
|
|
// 기본값: 폴더
|
|
return { icon: Folder, color: 'text-yellow-400', bgColor: 'from-yellow-500/30 to-yellow-600/30' };
|
|
}
|
|
|
|
export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
|
|
const [favorites, setFavorites] = useState<FavoriteItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
loadFavorites();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && isOpen) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
window.addEventListener('keydown', handleEscape);
|
|
return () => window.removeEventListener('keydown', handleEscape);
|
|
}
|
|
}, [isOpen, onClose]);
|
|
|
|
const loadFavorites = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await comms.getFavoriteList();
|
|
if (response.Success && response.Data) {
|
|
// 이름으로 정렬
|
|
const sorted = (response.Data as FavoriteItem[]).sort((a, b) =>
|
|
a.name.localeCompare(b.name, 'ko')
|
|
);
|
|
setFavorites(sorted);
|
|
}
|
|
} catch (error) {
|
|
console.error('즐겨찾기 로드 오류:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleItemClick = (url: string) => {
|
|
window.open(url, '_blank');
|
|
onClose();
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Dialog */}
|
|
<div className="relative w-full max-w-4xl mx-4 glass-effect-solid rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
|
<div className="flex items-center gap-3">
|
|
<Star className="w-5 h-5 text-yellow-400" />
|
|
<h2 className="text-lg font-semibold text-white">즐겨찾기</h2>
|
|
<span className="text-white/50 text-sm">({favorites.length}개)</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg text-white/60 hover:text-white hover:bg-white/10 transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 max-h-[70vh] overflow-auto">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60" />
|
|
</div>
|
|
) : favorites.length === 0 ? (
|
|
<div className="text-center py-12 text-white/50">
|
|
등록된 즐겨찾기가 없습니다.
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
|
{favorites.map((item, index) => {
|
|
const { icon: Icon, color, bgColor } = getIconInfo(item.url);
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={() => handleItemClick(item.url)}
|
|
className="group flex flex-col items-center p-4 rounded-xl bg-white/5 hover:bg-white/15 border border-white/10 hover:border-white/30 transition-all duration-200 hover:scale-105"
|
|
>
|
|
<div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${bgColor} flex items-center justify-center mb-3 group-hover:opacity-80 transition-opacity`}>
|
|
<Icon className={`w-5 h-5 ${color}`} />
|
|
</div>
|
|
<span className="text-sm text-white/80 group-hover:text-white text-center line-clamp-2 leading-tight">
|
|
{item.name}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-3 border-t border-white/10 bg-black/20">
|
|
<p className="text-xs text-white/40 text-center">
|
|
클릭하면 새 탭에서 열립니다
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|