From eb1ed7f7e807a4a550285064e96169621e011a42 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 6 Nov 2025 18:09:26 +0900
Subject: (김준회) 결재 preview dialog 공통컴포넌트 UI 수정, 렌더링 사이클 오류
 수정(useEffect 의존성변수에 의한 무한로딩)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/approval/approval-preview-dialog.tsx | 205 ++++++++++++-------------------
 1 file changed, 81 insertions(+), 124 deletions(-)

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

