From 18ca4ad784aeeab9ab7a13bbc8b3c13b42ca5e49 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Fri, 7 Nov 2025 12:01:16 +0900
Subject: (김준회) 결재 미리보기 공통컴포넌트 중복 제거, 기존 코드의 미리보기
 호출부 수정, 템플릿 작성 가이드 간략히 추가, 결재 미리보기시 첨부파일 편집
 처리
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/approval/approval-preview-dialog.tsx | 156 +++++++++++++++++++++++++++++++
 1 file changed, 156 insertions(+)

(limited to 'lib/approval/approval-preview-dialog.tsx')

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>
-- 
cgit v1.2.3

