diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-06 18:09:26 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-06 18:09:26 +0900 |
| commit | eb1ed7f7e807a4a550285064e96169621e011a42 (patch) | |
| tree | 92a405d86dafc6a02598005f7672156b32a7f1ce | |
| parent | e9879fc4808eb713d5c92263e7c8b37c2843be12 (diff) | |
(김준회) 결재 preview dialog 공통컴포넌트 UI 수정, 렌더링 사이클 오류 수정(useEffect 의존성변수에 의한 무한로딩)
| -rw-r--r-- | lib/approval/approval-preview-dialog.tsx | 205 | ||||
| -rw-r--r-- | lib/vendors/table/approve-vendor-dialog.tsx | 4 |
2 files changed, 83 insertions, 126 deletions
diff --git a/lib/approval/approval-preview-dialog.tsx b/lib/approval/approval-preview-dialog.tsx index bc5a4f65..a91e146c 100644 --- a/lib/approval/approval-preview-dialog.tsx +++ b/lib/approval/approval-preview-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; -import { Loader2, Eye, Send, X } from "lucide-react"; +import { Loader2, Send, X } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -23,19 +23,18 @@ import { } from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useMediaQuery } from "@/hooks/use-media-query"; -import { ApprovalLineSelector } from "@/components/knox/approval/ApprovalLineSelector"; +import { + ApprovalLineSelector, + type ApprovalLineItem +} from "@/components/knox/approval/ApprovalLineSelector"; import { getApprovalTemplateByName, replaceTemplateVariables } from "./template-utils"; -import type { ApprovalLine } from "@/components/knox/approval/types"; - /** * 결재 미리보기 다이얼로그 Props */ @@ -50,8 +49,6 @@ export interface ApprovalPreviewDialogProps { variables: Record<string, string>; /** 결재 제목 */ title: string; - /** 결재 설명 (선택사항) */ - description?: string; /** 현재 사용자 정보 */ currentUser: { id: number; @@ -66,12 +63,9 @@ export interface ApprovalPreviewDialogProps { onConfirm: (data: { approvers: string[]; title: string; - description?: string; }) => Promise<void>; /** 제목 수정 가능 여부 (기본: true) */ allowTitleEdit?: boolean; - /** 설명 수정 가능 여부 (기본: true) */ - allowDescriptionEdit?: boolean; } /** @@ -104,12 +98,10 @@ export function ApprovalPreviewDialog({ templateName, variables, title: initialTitle, - description: initialDescription, currentUser, defaultApprovers = [], onConfirm, allowTitleEdit = true, - allowDescriptionEdit = true, }: ApprovalPreviewDialogProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -119,8 +111,7 @@ export function ApprovalPreviewDialog({ // 폼 상태 const [title, setTitle] = React.useState(initialTitle); - const [description, setDescription] = React.useState(initialDescription || ""); - const [approvalLines, setApprovalLines] = React.useState<ApprovalLine[]>([]); + const [approvalLines, setApprovalLines] = React.useState<ApprovalLineItem[]>([]); const [previewHtml, setPreviewHtml] = React.useState<string>(""); // 템플릿 로딩 및 미리보기 생성 @@ -154,14 +145,24 @@ export function ApprovalPreviewDialog({ } loadTemplatePreview(); - }, [open, templateName, variables]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, templateName]); // variables 제거 - 다이얼로그가 열릴 때만 로드 - // 초기 결재선 설정 + // 다이얼로그 상태 초기화/리셋 React.useEffect(() => { - if (!open) return; + if (!open) { + // 다이얼로그가 닫힐 때 상태 초기화 + setTitle(initialTitle); + setApprovalLines([]); + setPreviewHtml(""); + return; + } + + // 다이얼로그가 열릴 때 초기화 + setTitle(initialTitle); // 상신자 추가 - const submitter: ApprovalLine = { + const submitter: ApprovalLineItem = { id: `submitter-${currentUser.id}`, epId: currentUser.epId, userId: currentUser.id.toString(), @@ -174,7 +175,7 @@ export function ApprovalPreviewDialog({ }; // 기본 결재자들 추가 (있는 경우) - const defaultLines: ApprovalLine[] = defaultApprovers.map((epId, index) => ({ + const defaultLines: ApprovalLineItem[] = defaultApprovers.map((epId, index) => ({ id: `approver-${index}`, epId: epId, userId: "", // EP ID로만 식별 @@ -186,10 +187,11 @@ export function ApprovalPreviewDialog({ })); setApprovalLines([submitter, ...defaultLines]); - }, [open, currentUser, defaultApprovers]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); // open 상태만 감지 - 다이얼로그 열림/닫힘 시에만 초기화 // 결재선 변경 핸들러 - const handleApprovalLinesChange = (lines: ApprovalLine[]) => { + const handleApprovalLinesChange = (lines: ApprovalLineItem[]) => { setApprovalLines(lines); }; @@ -223,7 +225,6 @@ export function ApprovalPreviewDialog({ await onConfirm({ approvers: approverEpIds, title: title.trim(), - description: description.trim() || undefined, }); // 성공 시 다이얼로그 닫기 @@ -244,99 +245,55 @@ export function ApprovalPreviewDialog({ // 폼 내용 const FormContent = () => ( <div className="space-y-6"> - {/* 탭: 미리보기 / 결재선 설정 */} - <Tabs defaultValue="preview" className="w-full"> - <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="preview" className="gap-2"> - <Eye className="size-4" /> - 미리보기 - </TabsTrigger> - <TabsTrigger value="approvers" className="gap-2"> - <Send className="size-4" /> - 결재선 설정 - </TabsTrigger> - </TabsList> - - {/* 미리보기 탭 */} - <TabsContent value="preview" className="space-y-4 mt-4"> - {/* 제목 입력 */} - <div className="space-y-2"> - <Label htmlFor="title">결재 제목</Label> - <Input - id="title" - value={title} - onChange={(e) => setTitle(e.target.value)} - placeholder="결재 제목을 입력하세요" - disabled={!allowTitleEdit || isSubmitting} - /> - </div> + {/* 결재선 설정 */} + <div className="space-y-4"> + <div className="space-y-2"> + <Label>결재선</Label> + <p className="text-sm text-muted-foreground"> + 결재자를 검색하여 추가하고, 결재 순서를 설정하세요. + </p> + </div> - {/* 설명 입력 */} - <div className="space-y-2"> - <Label htmlFor="description">결재 설명 (선택사항)</Label> - <Textarea - id="description" - value={description} - onChange={(e) => setDescription(e.target.value)} - placeholder="결재 설명을 입력하세요" - rows={3} - disabled={!allowDescriptionEdit || isSubmitting} + <ApprovalLineSelector + value={approvalLines} + onChange={handleApprovalLinesChange} + placeholder="결재자를 검색하세요..." + maxSelections={10} + domainFilter={{ type: "exclude", domains: ["partners"] }} + /> + </div> + + {/* 제목 입력 */} + <div className="space-y-2"> + <Label htmlFor="title">결재 제목</Label> + <Input + id="title" + value={title} + onChange={(e) => setTitle(e.target.value)} + placeholder="결재 제목을 입력하세요" + disabled={!allowTitleEdit || isSubmitting} + /> + </div> + + {/* 템플릿 미리보기 */} + <div className="space-y-2"> + <Label>문서 미리보기</Label> + <ScrollArea className="h-[400px] w-full rounded-md border bg-gray-50 p-4"> + {isLoadingTemplate ? ( + <div className="flex items-center justify-center h-full"> + <Loader2 className="size-6 animate-spin text-muted-foreground" /> + <span className="ml-2 text-sm text-muted-foreground"> + 템플릿을 불러오는 중... + </span> + </div> + ) : ( + <div + className="prose prose-sm max-w-none" + dangerouslySetInnerHTML={{ __html: previewHtml }} /> - </div> - - {/* 템플릿 미리보기 */} - <div className="space-y-2"> - <Label>문서 미리보기</Label> - <ScrollArea className="h-[300px] w-full rounded-md border bg-gray-50 p-4"> - {isLoadingTemplate ? ( - <div className="flex items-center justify-center h-full"> - <Loader2 className="size-6 animate-spin text-muted-foreground" /> - <span className="ml-2 text-sm text-muted-foreground"> - 템플릿을 불러오는 중... - </span> - </div> - ) : ( - <div - className="prose prose-sm max-w-none" - dangerouslySetInnerHTML={{ __html: previewHtml }} - /> - )} - </ScrollArea> - </div> - </TabsContent> - - {/* 결재선 설정 탭 */} - <TabsContent value="approvers" className="space-y-4 mt-4"> - <div className="space-y-2"> - <Label>결재선</Label> - <p className="text-sm text-muted-foreground"> - 결재자를 검색하여 추가하고, 결재 순서를 설정하세요. - </p> - </div> - - <ApprovalLineSelector - value={approvalLines} - onChange={handleApprovalLinesChange} - placeholder="결재자를 검색하세요..." - maxSelections={10} - domainFilter={{ type: "exclude", domains: ["partners"] }} - /> - - {/* 결재선 요약 */} - <div className="rounded-md bg-blue-50 p-3 border border-blue-200"> - <p className="text-sm font-medium text-blue-900 mb-1"> - 📋 결재 경로 - </p> - <p className="text-sm text-blue-700"> - {approvalLines - .filter((line) => line.seq !== "0") - .sort((a, b) => parseInt(a.seq) - parseInt(b.seq)) - .map((line) => line.name) - .join(" → ") || "결재자를 선택해주세요"} - </p> - </div> - </TabsContent> - </Tabs> + )} + </ScrollArea> + </div> </div> ); @@ -344,19 +301,19 @@ export function ApprovalPreviewDialog({ if (isDesktop) { return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"> - <DialogHeader> - <DialogTitle>결재 미리보기</DialogTitle> + <DialogContent className="max-w-4xl h-[90vh] flex flex-col p-0"> + <DialogHeader className="px-6 pt-6 pb-4 border-b"> + <DialogTitle>결재 문서 미리보기</DialogTitle> <DialogDescription> 결재 문서를 확인하고 결재선을 설정한 후 상신하세요. </DialogDescription> </DialogHeader> - <div className="flex-1 overflow-y-auto"> + <div className="flex-1 overflow-y-auto px-6 py-4"> <FormContent /> </div> - <DialogFooter className="gap-2 sm:space-x-0"> + <DialogFooter className="px-6 py-4 border-t gap-2 sm:space-x-0"> <Button variant="outline" onClick={handleCancel} @@ -390,19 +347,19 @@ export function ApprovalPreviewDialog({ // Mobile: Drawer return ( <Drawer open={open} onOpenChange={onOpenChange}> - <DrawerContent className="max-h-[90vh]"> - <DrawerHeader> - <DrawerTitle>결재 미리보기</DrawerTitle> + <DrawerContent className="h-[90vh] flex flex-col"> + <DrawerHeader className="border-b"> + <DrawerTitle>결재 문서 미리보기</DrawerTitle> <DrawerDescription> 결재 문서를 확인하고 결재선을 설정한 후 상신하세요. </DrawerDescription> </DrawerHeader> - <div className="px-4 overflow-y-auto"> + <div className="flex-1 overflow-y-auto px-4 py-4"> <FormContent /> </div> - <DrawerFooter className="gap-2"> + <DrawerFooter className="border-t gap-2"> <Button variant="outline" onClick={handleCancel} diff --git a/lib/vendors/table/approve-vendor-dialog.tsx b/lib/vendors/table/approve-vendor-dialog.tsx index fea5a006..9adcbf06 100644 --- a/lib/vendors/table/approve-vendor-dialog.tsx +++ b/lib/vendors/table/approve-vendor-dialog.tsx @@ -275,7 +275,7 @@ export function VendorDecisionDialog({ </Dialog> {/* 결재 미리보기 다이얼로그 */} - {previewData && session?.user?.epId && ( + {previewData && session?.user?.epId && showPreview && ( <ApprovalPreviewDialog open={showPreview} onOpenChange={setShowPreview} @@ -369,7 +369,7 @@ export function VendorDecisionDialog({ </Drawer> {/* 결재 미리보기 다이얼로그 */} - {previewData && session?.user?.epId && ( + {previewData && session?.user?.epId && showPreview && ( <ApprovalPreviewDialog open={showPreview} onOpenChange={setShowPreview} |
