diff options
Diffstat (limited to 'lib/swp/table')
| -rw-r--r-- | lib/swp/table/swp-help-dialog.tsx | 153 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-columns.tsx | 83 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 139 |
3 files changed, 351 insertions, 24 deletions
diff --git a/lib/swp/table/swp-help-dialog.tsx b/lib/swp/table/swp-help-dialog.tsx new file mode 100644 index 00000000..18f29644 --- /dev/null +++ b/lib/swp/table/swp-help-dialog.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { HelpCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; + +export function SwpUploadHelpDialog() { + return ( + <Dialog> + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <HelpCircle className="h-4 w-4" /> + 업로드 가이드 + </Button> + </DialogTrigger> + <DialogContent className="max-w-2xl" opacityControl={false}> + <DialogHeader> + <DialogTitle>파일 업로드 가이드</DialogTitle> + <DialogDescription> + 올바른 파일명 형식으로 업로드해주세요 + </DialogDescription> + </DialogHeader> + + <div className="space-y-6"> + {/* 파일명 형식 */} + <div className="space-y-2"> + <h3 className="text-sm font-semibold">📋 파일명 형식</h3> + <div className="rounded-lg bg-muted p-4 font-mono text-sm"> + [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].[확장자] + </div> + <p className="text-xs text-muted-foreground"> + ⚠️ 언더스코어(_)가 정확히 3개 있어야 합니다 + </p> + </div> + + {/* 각 항목 설명 - 1라인 형태 */} + <div className="space-y-3"> + <h3 className="text-sm font-semibold">📝 항목 설명</h3> + + <div className="flex items-center gap-3 rounded-lg border p-3"> + <Badge variant="secondary" className="font-mono shrink-0"> + OWN_DOC_NO + </Badge> + <div className="text-sm"> + <span className="font-medium">벤더의 문서번호</span> + <span className="text-muted-foreground"> - 프로젝트마다 유니크해야 합니다</span> + </div> + </div> + + <div className="flex items-center gap-3 rounded-lg border p-3"> + <Badge variant="secondary" className="font-mono shrink-0"> + REV_NO + </Badge> + <div className="text-sm"> + <span className="font-medium">리비전 번호</span> + <span className="text-muted-foreground"> - 보통 01, 02 같은 식으로 피드백에 따라 증가합니다</span> + </div> + </div> + + <div className="flex items-center gap-3 rounded-lg border p-3"> + <Badge variant="secondary" className="font-mono shrink-0"> + STAGE + </Badge> + <div className="text-sm"> + <span className="font-medium">스테이지</span> + <span className="text-muted-foreground"> - 중공업이 설정한 스테이지입니다 (예: IFA, IFC, AFC, BFC)</span> + </div> + </div> + + <div className="flex items-center gap-3 rounded-lg border p-3"> + <Badge variant="secondary" className="font-mono shrink-0"> + YYYYMMDDhhmmss + </Badge> + <div className="text-sm"> + <span className="font-medium">날짜 및 시간</span> + <span className="text-muted-foreground"> - 업로드 날짜 정보를 기입합니다 (14자리 숫자)</span> + </div> + </div> + </div> + + {/* 예시 */} + <div className="space-y-2"> + <h3 className="text-sm font-semibold">✅ 올바른 예시</h3> + <div className="space-y-2"> + <div className="rounded-lg bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 p-3"> + <code className="text-xs font-mono text-green-700 dark:text-green-300"> + VD-DOC-001_01_IFA_20250124143000.pdf + </code> + </div> + <div className="rounded-lg bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 p-3"> + <code className="text-xs font-mono text-green-700 dark:text-green-300"> + TECH-SPEC-002_02_IFC_20250124150000.dwg + </code> + </div> + </div> + </div> + + {/* 잘못된 예시 */} + <div className="space-y-2"> + <h3 className="text-sm font-semibold">❌ 잘못된 예시</h3> + <div className="space-y-2"> + <div className="rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 p-3"> + <code className="text-xs font-mono text-red-700 dark:text-red-300"> + VD-DOC-001-01-IFA-20250124.pdf + </code> + <p className="text-xs text-red-600 dark:text-red-400 mt-1"> + ✗ 언더스코어(_) 대신 하이픈(-) 사용 + </p> + </div> + <div className="rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 p-3"> + <code className="text-xs font-mono text-red-700 dark:text-red-300"> + VD-DOC-001_01_IFA.pdf + </code> + <p className="text-xs text-red-600 dark:text-red-400 mt-1"> + ✗ 날짜/시간 정보 누락 + </p> + </div> + <div className="rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 p-3"> + <code className="text-xs font-mono text-red-700 dark:text-red-300"> + VD-DOC-001_01_IFA_20250124.pdf + </code> + <p className="text-xs text-red-600 dark:text-red-400 mt-1"> + ✗ 시간 정보 누락 (14자리가 아님) + </p> + </div> + </div> + </div> + + {/* 주의사항 */} + <div className="rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 p-4"> + <h3 className="text-sm font-semibold text-amber-900 dark:text-amber-100 mb-2"> + ⚠️ 주의사항 + </h3> + <ul className="text-xs text-amber-800 dark:text-amber-200 space-y-1 list-disc list-inside"> + <li>파일명 형식이 올바르지 않으면 업로드가 실패합니다</li> + <li>같은 파일명으로 이미 업로드된 파일이 있으면 덮어쓰지 않고 오류 처리됩니다</li> + <li>프로젝트와 업체 코드를 먼저 선택해야 업로드 버튼이 활성화됩니다</li> + </ul> + </div> + </div> + </DialogContent> + </Dialog> + ); +} + diff --git a/lib/swp/table/swp-table-columns.tsx b/lib/swp/table/swp-table-columns.tsx index b18e2b27..573acf1b 100644 --- a/lib/swp/table/swp-table-columns.tsx +++ b/lib/swp/table/swp-table-columns.tsx @@ -3,10 +3,13 @@ import { ColumnDef } from "@tanstack/react-table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { ChevronDown, ChevronRight, FileIcon, Download } from "lucide-react"; +import { ChevronDown, ChevronRight, FileIcon, Download, Loader2 } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { ko } from "date-fns/locale"; import type { SwpDocumentWithStats } from "../actions"; +import { downloadSwpFile } from "../actions"; +import { useState } from "react"; +import { toast } from "sonner"; export const swpDocumentColumns: ColumnDef<SwpDocumentWithStats>[] = [ { @@ -388,19 +391,75 @@ export const swpFileColumns: ColumnDef<FileRow>[] = [ id: "actions", header: "작업", cell: ({ row }) => ( - <Button - variant="outline" - size="sm" - onClick={() => { - // TODO: 파일 다운로드 로직 구현 - console.log("Download file:", row.original.FILE_NM); - }} - > - <Download className="h-4 w-4 mr-1" /> - 다운로드 - </Button> + <DownloadButton fileId={row.original.id} fileName={row.original.FILE_NM} /> ), size: 120, }, ]; +// ============================================================================ +// 다운로드 버튼 컴포넌트: 임시 구성. Download.aspx 동작 안해서 일단 네트워크드라이브 사용하도록 처리 +// ============================================================================ + +interface DownloadButtonProps { + fileId: number; + fileName: string; +} + +function DownloadButton({ fileId, fileName }: DownloadButtonProps) { + const [isDownloading, setIsDownloading] = useState(false); + + const handleDownload = async () => { + try { + setIsDownloading(true); + + // 서버 액션 호출 + const result = await downloadSwpFile(fileId); + + if (!result.success || !result.data) { + toast.error(result.error || "파일 다운로드 실패"); + return; + } + + // Blob 생성 및 다운로드 + const blob = new Blob([result.data as unknown as BlobPart], { type: result.mimeType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = result.fileName || fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + toast.success(`파일 다운로드 완료: ${result.fileName}`); + } catch (error) { + console.error("다운로드 오류:", error); + toast.error("파일 다운로드 중 오류가 발생했습니다."); + } finally { + setIsDownloading(false); + } + }; + + return ( + <Button + variant="outline" + size="sm" + onClick={handleDownload} + disabled={isDownloading} + > + {isDownloading ? ( + <> + <Loader2 className="h-4 w-4 mr-1 animate-spin" /> + 다운로드 중... + </> + ) : ( + <> + <Download className="h-4 w-4 mr-1" /> + 다운로드 + </> + )} + </Button> + ); +} + diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx index 6858f42e..03082b26 100644 --- a/lib/swp/table/swp-table-toolbar.tsx +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -16,11 +16,13 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; -import { RefreshCw, Download, Search, X, Check, ChevronsUpDown } from "lucide-react"; -import { syncSwpProjectAction, type SwpTableFilters } from "../actions"; +import { RefreshCw, Search, X, Check, ChevronsUpDown, Upload } from "lucide-react"; +import { syncSwpProjectAction, uploadSwpFilesAction, type SwpTableFilters } from "../actions"; import { useToast } from "@/hooks/use-toast"; import { useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; +import { useRef } from "react"; +import { SwpUploadHelpDialog } from "./swp-help-dialog"; interface SwpTableToolbarProps { filters: SwpTableFilters; @@ -34,11 +36,13 @@ export function SwpTableToolbar({ projects = [], }: SwpTableToolbarProps) { const [isSyncing, startSync] = useTransition(); + const [isUploading, startUpload] = useTransition(); const [localFilters, setLocalFilters] = useState<SwpTableFilters>(filters); const { toast } = useToast(); const router = useRouter(); const [projectSearchOpen, setProjectSearchOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); + const fileInputRef = useRef<HTMLInputElement>(null); // 동기화 핸들러 const handleSync = () => { @@ -86,17 +90,112 @@ export function SwpTableToolbar({ /** * 파일 업로드 핸들러 - * 1) 네트워크 드라이브에 정해진 규칙대로, 파일이름 기반으로 파일 업로드하기 (단, cpyCd는 어떻게 해결할지 고민해봐야 함...) + * 1) 네트워크 드라이브에 정해진 규칙대로, 파일이름 기반으로 파일 업로드하기 * 2) 1~N개 파일 받아서, 파일 이름 기준으로 파싱해서 SaveInBoxList API를 통해 업로드 처리 - * - * 개발중인 동안은 토스트 반환하도록 처리 */ const handleUploadFiles = () => { - toast({ - title: "파일 업로드", - description: "현재 개발중입니다.", + // 프로젝트와 벤더 코드 체크 + const projectNo = localFilters.projNo; + const vndrCd = localFilters.vndrCd; + + if (!projectNo) { + toast({ + variant: "destructive", + title: "프로젝트 선택 필요", + description: "파일을 업로드할 프로젝트를 먼저 선택해주세요.", + }); + return; + } + + if (!vndrCd) { + toast({ + variant: "destructive", + title: "업체 코드 입력 필요", + description: "파일을 업로드할 업체 코드를 입력해주세요.", + }); + return; + } + + // 파일 선택 다이얼로그 열기 + fileInputRef.current?.click(); + }; + + /** + * 파일 선택 핸들러 + */ + const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { + const selectedFiles = event.target.files; + if (!selectedFiles || selectedFiles.length === 0) { + return; + } + + const projectNo = localFilters.projNo!; + const vndrCd = localFilters.vndrCd!; + + startUpload(async () => { + try { + toast({ + title: "파일 업로드 시작", + description: `${selectedFiles.length}개 파일을 업로드합니다...`, + }); + + // 파일을 Buffer로 변환 + const fileInfos = await Promise.all( + Array.from(selectedFiles).map(async (file) => { + const arrayBuffer = await file.arrayBuffer(); + return { + fileName: file.name, + fileBuffer: Buffer.from(arrayBuffer), + }; + }) + ); + + // 서버 액션 호출 + const result = await uploadSwpFilesAction(projectNo, vndrCd, fileInfos); + + if (result.success) { + toast({ + title: "업로드 완료", + description: result.message, + }); + + // 페이지 새로고침 + router.refresh(); + } else { + toast({ + variant: "destructive", + title: "업로드 실패", + description: result.message, + }); + } + + // 실패한 파일이 있으면 상세 정보 표시 + const failedFiles = result.details.filter((d) => !d.success); + if (failedFiles.length > 0) { + console.error("실패한 파일:", failedFiles); + failedFiles.forEach((f) => { + toast({ + variant: "destructive", + title: `${f.fileName} 업로드 실패`, + description: f.error || "알 수 없는 오류", + }); + }); + } + } catch (error) { + console.error("파일 업로드 실패:", error); + toast({ + variant: "destructive", + title: "업로드 실패", + description: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } finally { + // 파일 입력 초기화 + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } }); - } + }; // 검색 적용 const handleSearch = () => { @@ -141,10 +240,26 @@ export function SwpTableToolbar({ <div className="text-sm text-muted-foreground"> SWP 문서 관리 시스템 </div> - <div> - <Button variant="outline" size="sm" onClick={handleUploadFiles}> - 파일 업로드하기 + <div className="flex items-center gap-2"> + <input + ref={fileInputRef} + type="file" + multiple + className="hidden" + onChange={handleFileChange} + accept="*/*" + /> + <Button + variant="outline" + size="sm" + onClick={handleUploadFiles} + disabled={isUploading || !localFilters.projNo || !localFilters.vndrCd} + > + <Upload className={`h-4 w-4 mr-2 ${isUploading ? "animate-pulse" : ""}`} /> + {isUploading ? "업로드 중..." : "파일 업로드"} </Button> + + <SwpUploadHelpDialog /> </div> </div> |
