summaryrefslogtreecommitdiff
path: root/lib/approval/approval-preview-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/approval/approval-preview-dialog.tsx')
-rw-r--r--lib/approval/approval-preview-dialog.tsx156
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>