summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-06 18:09:26 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-06 18:09:26 +0900
commiteb1ed7f7e807a4a550285064e96169621e011a42 (patch)
tree92a405d86dafc6a02598005f7672156b32a7f1ce
parente9879fc4808eb713d5c92263e7c8b37c2843be12 (diff)
(김준회) 결재 preview dialog 공통컴포넌트 UI 수정, 렌더링 사이클 오류 수정(useEffect 의존성변수에 의한 무한로딩)
-rw-r--r--lib/approval/approval-preview-dialog.tsx205
-rw-r--r--lib/vendors/table/approve-vendor-dialog.tsx4
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}