diff options
Diffstat (limited to 'lib/approval/approval-preview-dialog.tsx')
| -rw-r--r-- | lib/approval/approval-preview-dialog.tsx | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/lib/approval/approval-preview-dialog.tsx b/lib/approval/approval-preview-dialog.tsx index a91e146c..8bb7ba0f 100644 --- a/lib/approval/approval-preview-dialog.tsx +++ b/lib/approval/approval-preview-dialog.tsx @@ -25,6 +25,29 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useMediaQuery } from "@/hooks/use-media-query"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Paperclip } from "lucide-react"; +import { Separator } from "@/components/ui/separator"; +import prettyBytes from "pretty-bytes"; +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone"; +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list"; import { ApprovalLineSelector, @@ -63,9 +86,16 @@ export interface ApprovalPreviewDialogProps { onConfirm: (data: { approvers: string[]; title: string; + attachments?: File[]; }) => Promise<void>; /** 제목 수정 가능 여부 (기본: true) */ allowTitleEdit?: boolean; + /** 첨부파일 UI 활성화 여부 (기본: false) */ + enableAttachments?: boolean; + /** 최대 첨부파일 개수 (기본: 10) */ + maxAttachments?: number; + /** 최대 파일 크기 (기본: 100MB) */ + maxFileSize?: number; } /** @@ -102,6 +132,9 @@ export function ApprovalPreviewDialog({ defaultApprovers = [], onConfirm, allowTitleEdit = true, + enableAttachments = false, + maxAttachments = 10, + maxFileSize = 100 * 1024 * 1024, // 100MB }: ApprovalPreviewDialogProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -113,6 +146,7 @@ export function ApprovalPreviewDialog({ const [title, setTitle] = React.useState(initialTitle); const [approvalLines, setApprovalLines] = React.useState<ApprovalLineItem[]>([]); const [previewHtml, setPreviewHtml] = React.useState<string>(""); + const [attachments, setAttachments] = React.useState<File[]>([]); // 템플릿 로딩 및 미리보기 생성 React.useEffect(() => { @@ -155,6 +189,7 @@ export function ApprovalPreviewDialog({ setTitle(initialTitle); setApprovalLines([]); setPreviewHtml(""); + setAttachments([]); return; } @@ -195,6 +230,36 @@ export function ApprovalPreviewDialog({ setApprovalLines(lines); }; + // 파일 드롭 핸들러 + const handleDropAccepted = React.useCallback( + (files: File[]) => { + if (attachments.length + files.length > maxAttachments) { + toast.error(`최대 ${maxAttachments}개의 파일만 첨부할 수 있습니다.`); + return; + } + + // 중복 파일 체크 + const newFiles = files.filter( + (file) => !attachments.some((existing) => existing.name === file.name && existing.size === file.size) + ); + + if (newFiles.length !== files.length) { + toast.warning("일부 중복된 파일은 제외되었습니다."); + } + + setAttachments((prev) => [...prev, ...newFiles]); + }, + [attachments, maxAttachments] + ); + + const handleDropRejected = React.useCallback(() => { + toast.error(`파일 크기는 ${prettyBytes(maxFileSize)} 이하여야 합니다.`); + }, [maxFileSize]); + + const handleRemoveFile = React.useCallback((index: number) => { + setAttachments((prev) => prev.filter((_, i) => i !== index)); + }, []); + // 제출 핸들러 const handleSubmit = async () => { try { @@ -225,6 +290,7 @@ export function ApprovalPreviewDialog({ await onConfirm({ approvers: approverEpIds, title: title.trim(), + attachments: enableAttachments ? attachments : undefined, }); // 성공 시 다이얼로그 닫기 @@ -275,6 +341,96 @@ export function ApprovalPreviewDialog({ /> </div> + {/* 첨부파일 섹션 (enableAttachments가 true일 때만 표시) */} + {enableAttachments && ( + <> + <Separator /> + + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Paperclip className="w-4 h-4" /> + 첨부파일 + {attachments.length > 0 && ( + <span className="text-sm font-normal text-muted-foreground"> + ({attachments.length}/{maxAttachments}) + </span> + )} + </CardTitle> + <CardDescription> + 결재 문서에 첨부할 파일을 추가하세요 (최대 {maxAttachments}개, 파일당 최대 {prettyBytes(maxFileSize)}) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + {/* 파일 드롭존 */} + {attachments.length < maxAttachments && ( + <Dropzone + maxSize={maxFileSize} + onDropAccepted={handleDropAccepted} + onDropRejected={handleDropRejected} + disabled={isSubmitting} + > + {() => ( + <DropzoneZone className="flex justify-center h-24"> + <DropzoneInput /> + <div className="flex items-center gap-4"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle> + <DropzoneDescription> + 모든 형식의 파일을 첨부할 수 있습니다 + </DropzoneDescription> + </div> + </div> + </DropzoneZone> + )} + </Dropzone> + )} + + {/* 첨부된 파일 목록 */} + {attachments.length > 0 && ( + <FileList> + {attachments.map((file, index) => ( + <FileListItem key={`${file.name}-${index}`}> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <div className="flex-1"> + <FileListName>{file.name}</FileListName> + <FileListDescription> + <FileListSize>{file.size}</FileListSize> + {file.type && ( + <> + <span>•</span> + <span>{file.type}</span> + </> + )} + </FileListDescription> + </div> + </FileListInfo> + </FileListHeader> + <FileListAction + onClick={() => handleRemoveFile(index)} + disabled={isSubmitting} + title="파일 제거" + > + <X className="w-4 h-4" /> + </FileListAction> + </FileListItem> + ))} + </FileList> + )} + + {attachments.length === 0 && ( + <p className="text-sm text-muted-foreground text-center py-4"> + 첨부된 파일이 없습니다 + </p> + )} + </CardContent> + </Card> + </> + )} + {/* 템플릿 미리보기 */} <div className="space-y-2"> <Label>문서 미리보기</Label> |
