summaryrefslogtreecommitdiff
path: root/components/ui
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-07 01:43:36 +0000
commitfbb3b7f05737f9571b04b0a8f4f15c0928de8545 (patch)
tree343247117a7587b8ef5c418c9528d1cf2e0b6f1c /components/ui
parent9945ad119686a4c3a66f7b57782750f78a366cfb (diff)
(대표님) 변경사항 20250707 10시 43분
Diffstat (limited to 'components/ui')
-rw-r--r--components/ui/file-actions.tsx440
-rw-r--r--components/ui/text-utils.tsx131
2 files changed, 571 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
diff --git a/components/ui/text-utils.tsx b/components/ui/text-utils.tsx
new file mode 100644
index 00000000..a3507dd0
--- /dev/null
+++ b/components/ui/text-utils.tsx
@@ -0,0 +1,131 @@
+"use client"
+
+import { useState } from "react"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
+import { ChevronDown, ChevronUp } from "lucide-react"
+
+export function TruncatedText({
+ text,
+ maxLength = 50,
+ showTooltip = true
+}: {
+ text: string | null
+ maxLength?: number
+ showTooltip?: boolean
+}) {
+ if (!text) return <span className="text-muted-foreground">-</span>
+
+ if (text.length <= maxLength) {
+ return <span>{text}</span>
+ }
+
+ const truncated = text.slice(0, maxLength) + "..."
+
+ if (!showTooltip) {
+ return <span>{truncated}</span>
+ }
+
+ return (
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <span className="cursor-help border-b border-dotted border-gray-400">
+ {truncated}
+ </span>
+ </TooltipTrigger>
+ <TooltipContent className="max-w-xs">
+ <p className="whitespace-pre-wrap">{text}</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ )
+}
+
+export function ExpandableText({
+ text,
+ maxLength = 100,
+ className = ""
+}: {
+ text: string | null
+ maxLength?: number
+ className?: string
+}) {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ if (!text) return <span className="text-muted-foreground">-</span>
+
+ if (text.length <= maxLength) {
+ return <span className={className}>{text}</span>
+ }
+
+ return (
+ <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
+ <div className={className}>
+ <CollapsibleTrigger asChild>
+ <button className="text-left w-full group">
+ <span className="whitespace-pre-wrap">
+ {isExpanded ? text : text.slice(0, maxLength) + "..."}
+ </span>
+ <span className="inline-flex items-center ml-2 text-blue-600 hover:text-blue-800">
+ {isExpanded ? (
+ <>
+ <ChevronUp className="w-3 h-3" />
+ <span className="text-xs ml-1">접기</span>
+ </>
+ ) : (
+ <>
+ <ChevronDown className="w-3 h-3" />
+ <span className="text-xs ml-1">더보기</span>
+ </>
+ )}
+ </span>
+ </button>
+ </CollapsibleTrigger>
+ </div>
+ </Collapsible>
+ )
+}
+
+export function AddressDisplay({
+ address,
+ addressEng,
+ postalCode,
+ addressDetail
+}: {
+ address: string | null
+ addressEng: string | null
+ postalCode: string | null
+ addressDetail: string | null
+}) {
+ const hasAnyAddress = address || addressEng || postalCode || addressDetail
+
+ if (!hasAnyAddress) {
+ return <span className="text-muted-foreground">-</span>
+ }
+
+ return (
+ <div className="space-y-1">
+ {postalCode && (
+ <div className="text-xs text-muted-foreground">
+ 우편번호: {postalCode}
+ </div>
+ )}
+ {address && (
+ <div className="font-medium break-words">
+ {address}
+ </div>
+ )}
+ {addressDetail && (
+ <div className="text-sm text-muted-foreground break-words">
+ {addressDetail}
+ </div>
+ )}
+ {addressEng && (
+ <div className="text-sm text-muted-foreground break-words italic">
+ {addressEng}
+ </div>
+ )}
+ </div>
+ )
+} \ No newline at end of file