feat: 품목정보 상세 패널 추가 및 프로젝트/근태/권한 기능 확장

- Items: 우측에 이미지, 담당자, 입고/발주내역 패널 추가 (fItems 윈폼 동일)
- Project: 목록 및 상세 다이얼로그 구현
- Kuntae: 오류검사/수정 기능 추가
- UserAuth: 사용자 권한 관리 페이지 추가
- UserGroup: 그룹정보 다이얼로그로 전환
- Header: 사용자 메뉴 서브메뉴 방향 수정, 즐겨찾기 기능
- Backend API: Items 상세/담당자/구매내역, 근태 오류검사, 프로젝트 목록 등

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-11-28 17:36:20 +09:00
parent c9b5d756e1
commit adcdc40169
32 changed files with 6668 additions and 292 deletions

View File

@@ -0,0 +1,188 @@
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]);
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>
);
}