diff options
Diffstat (limited to 'components/ui/file-actions.tsx')
| -rw-r--r-- | components/ui/file-actions.tsx | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/components/ui/file-actions.tsx b/components/ui/file-actions.tsx new file mode 100644 index 00000000..ed2103d3 --- /dev/null +++ b/components/ui/file-actions.tsx @@ -0,0 +1,440 @@ +// components/ui/file-actions.tsx +// 재사용 가능한 파일 액션 컴포넌트들 + +"use client"; + +import * as React from "react"; +import { + Download, + Eye, + Paperclip, + Loader2, + AlertCircle, + FileText, + Image as ImageIcon, + Archive +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import { useMultiFileDownload } from "@/hooks/use-file-download"; +import { getFileInfo, quickDownload, quickPreview, smartFileAction } from "@/lib/file-download"; +import { cn } from "@/lib/utils"; + +/** + * 파일 아이콘 컴포넌트 + */ +interface FileIconProps { + fileName: string; + className?: string; +} + +export const FileIcon: React.FC<FileIconProps> = ({ fileName, className }) => { + const fileInfo = getFileInfo(fileName); + + const iconMap = { + pdf: FileText, + document: FileText, + spreadsheet: FileText, + image: ImageIcon, + archive: Archive, + other: Paperclip, + }; + + const IconComponent = iconMap[fileInfo.type]; + + return ( + <IconComponent className={cn("h-4 w-4", className)} /> + ); +}; + +/** + * 기본 파일 다운로드 버튼 + */ +interface FileDownloadButtonProps { + filePath: string; + fileName: string; + variant?: "default" | "ghost" | "outline"; + size?: "default" | "sm" | "lg" | "icon"; + children?: React.ReactNode; + className?: string; + showIcon?: boolean; + disabled?: boolean; +} + +export const FileDownloadButton: React.FC<FileDownloadButtonProps> = ({ + filePath, + fileName, + variant = "ghost", + size = "icon", + children, + className, + showIcon = true, + disabled, +}) => { + const { downloadFile, isFileLoading, getFileError } = useMultiFileDownload(); + + const isLoading = isFileLoading(filePath); + const error = getFileError(filePath); + + const handleClick = () => { + if (!disabled && !isLoading) { + quickDownload(filePath, fileName); + } + }; + + if (isLoading) { + return ( + <Button variant={variant} size={size} disabled className={className}> + <Loader2 className="h-4 w-4 animate-spin" /> + {children} + </Button> + ); + } + + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={variant} + size={size} + onClick={handleClick} + disabled={disabled} + className={cn( + error && "text-destructive hover:text-destructive", + className + )} + > + {error ? ( + <AlertCircle className="h-4 w-4" /> + ) : showIcon ? ( + <Download className="h-4 w-4" /> + ) : null} + {children} + </Button> + </TooltipTrigger> + <TooltipContent> + {error ? `오류: ${error} (클릭하여 재시도)` : `${fileName} 다운로드`} + </TooltipContent> + </Tooltip> + </TooltipProvider> + ); +}; + +/** + * 미리보기 버튼 + */ +interface FilePreviewButtonProps extends Omit<FileDownloadButtonProps, 'children'> { + fallbackToDownload?: boolean; +} + +export const FilePreviewButton: React.FC<FilePreviewButtonProps> = ({ + filePath, + fileName, + variant = "ghost", + size = "icon", + className, + fallbackToDownload = true, + disabled, +}) => { + const { isFileLoading, getFileError } = useMultiFileDownload(); + const fileInfo = getFileInfo(fileName); + + const isLoading = isFileLoading(filePath); + const error = getFileError(filePath); + + const handleClick = () => { + if (!disabled && !isLoading) { + if (fileInfo.canPreview) { + quickPreview(filePath, fileName); + } else if (fallbackToDownload) { + quickDownload(filePath, fileName); + } + } + }; + + if (!fileInfo.canPreview && !fallbackToDownload) { + return ( + <Button variant={variant} size={size} disabled className={className}> + <Eye className="h-4 w-4 opacity-50" /> + </Button> + ); + } + + if (isLoading) { + return ( + <Button variant={variant} size={size} disabled className={className}> + <Loader2 className="h-4 w-4 animate-spin" /> + </Button> + ); + } + + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={variant} + size={size} + onClick={handleClick} + disabled={disabled} + className={cn( + error && "text-destructive hover:text-destructive", + className + )} + > + {error ? ( + <AlertCircle className="h-4 w-4" /> + ) : fileInfo.canPreview ? ( + <Eye className="h-4 w-4" /> + ) : ( + <Download className="h-4 w-4" /> + )} + </Button> + </TooltipTrigger> + <TooltipContent> + {error + ? `오류: ${error} (클릭하여 재시도)` + : fileInfo.canPreview + ? `${fileName} 미리보기` + : `${fileName} 다운로드` + } + </TooltipContent> + </Tooltip> + </TooltipProvider> + ); +}; + +/** + * 드롭다운 파일 액션 버튼 (미리보기 + 다운로드) + */ +interface FileActionsDropdownProps { + filePath: string; + fileName: string; + description?: string; + variant?: "default" | "ghost" | "outline"; + size?: "default" | "sm" | "lg" | "icon"; + className?: string; + disabled?: boolean; + triggerIcon?: React.ReactNode; +} + +export const FileActionsDropdown: React.FC<FileActionsDropdownProps> = ({ + filePath, + fileName, + variant = "ghost", + size = "icon", + className, + disabled, + triggerIcon, + description +}) => { + const { isFileLoading, getFileError } = useMultiFileDownload(); + const fileInfo = getFileInfo(fileName); + + const isLoading = isFileLoading(filePath); + const error = getFileError(filePath); + + const handlePreview = () => quickPreview(filePath, fileName); + const handleDownload = () => quickDownload(filePath, fileName); + + if (isLoading) { + return ( + <Button variant={variant} size={size} disabled className={className}> + <Loader2 className="h-4 w-4 animate-spin" /> + </Button> + ); + } + + if (error) { + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={variant} + size={size} + onClick={handleDownload} + className={cn("text-destructive hover:text-destructive", className)} + > + <AlertCircle className="h-4 w-4" /> + </Button> + </TooltipTrigger> + <TooltipContent> + <div className="text-sm"> + <div className="font-medium text-destructive">오류 발생</div> + <div className="text-muted-foreground">{error}</div> + <div className="mt-1 text-xs">클릭하여 재시도</div> + </div> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ); + } + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant={variant} + size={size} + disabled={disabled} + className={className} + > + {triggerIcon || <Paperclip className="h-4 w-4" />} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {fileInfo.canPreview && ( + <> + <DropdownMenuItem onClick={handlePreview}> + <Eye className="mr-2 h-4 w-4" /> + {fileInfo.icon} 미리보기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + </> + )} + <DropdownMenuItem onClick={handleDownload}> + <Download className="mr-2 h-4 w-4" /> + {description} 다운로드 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ); +}; + +/** + * 스마트 파일 액션 버튼 (자동 판단) + */ +interface SmartFileActionButtonProps extends Omit<FileDownloadButtonProps, 'children'> { + showLabel?: boolean; +} + +export const SmartFileActionButton: React.FC<SmartFileActionButtonProps> = ({ + filePath, + fileName, + variant = "ghost", + size = "icon", + className, + showLabel = false, + disabled, +}) => { + const { isFileLoading, getFileError } = useMultiFileDownload(); + const fileInfo = getFileInfo(fileName); + + const isLoading = isFileLoading(filePath); + const error = getFileError(filePath); + + const handleClick = () => { + if (!disabled && !isLoading) { + smartFileAction(filePath, fileName); + } + }; + + if (isLoading) { + return ( + <Button variant={variant} size={size} disabled className={className}> + <Loader2 className="h-4 w-4 animate-spin" /> + {showLabel && <span className="ml-2">처리 중...</span>} + </Button> + ); + } + + const actionText = fileInfo.canPreview ? '미리보기' : '다운로드'; + const IconComponent = fileInfo.canPreview ? Eye : Download; + + return ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <Button + variant={variant} + size={size} + onClick={handleClick} + disabled={disabled} + className={cn( + error && "text-destructive hover:text-destructive", + className + )} + > + {error ? ( + <AlertCircle className="h-4 w-4" /> + ) : ( + <IconComponent className="h-4 w-4" /> + )} + {showLabel && ( + <span className="ml-2"> + {error ? '재시도' : actionText} + </span> + )} + </Button> + </TooltipTrigger> + <TooltipContent> + {error + ? `오류: ${error} (클릭하여 재시도)` + : `${fileInfo.icon} ${fileName} ${actionText}` + } + </TooltipContent> + </Tooltip> + </TooltipProvider> + ); +}; + +/** + * 파일명 링크 컴포넌트 + */ +interface FileNameLinkProps { + filePath: string; + fileName: string; + className?: string; + showIcon?: boolean; + maxLength?: number; +} + +export const FileNameLink: React.FC<FileNameLinkProps> = ({ + filePath, + fileName, + className, + showIcon = true, + maxLength = 200, +}) => { + const fileInfo = getFileInfo(fileName); + + const handleClick = () => { + smartFileAction(filePath, fileName); + }; + + const displayName = fileName.length > maxLength + ? `${fileName.substring(0, maxLength)}...` + : fileName; + + return ( + <button + onClick={handleClick} + className={cn( + "flex items-center gap-1 text-blue-600 hover:text-blue-800 hover:underline cursor-pointer text-left", + className + )} + title={`${fileInfo.icon} ${fileName} ${fileInfo.canPreview ? '미리보기' : '다운로드'}`} + > + {showIcon && ( + <span className="text-xs flex-shrink-0">{fileInfo.icon}</span> + )} + <span className="truncate">{displayName}</span> + </button> + ); +};
\ No newline at end of file |
