diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
| commit | 78c471eec35182959e0029ded18f144974ccaca2 (patch) | |
| tree | 914cdf1c8f406ca3e2aa639b8bb774f7f4e87023 /components/approval | |
| parent | 0be8940580c4a4a4e098b649d198160f9b60420c (diff) | |
(김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰 결재 연결 예시 작성
Diffstat (limited to 'components/approval')
| -rw-r--r-- | components/approval/ApprovalPreviewDialog.tsx | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/components/approval/ApprovalPreviewDialog.tsx b/components/approval/ApprovalPreviewDialog.tsx new file mode 100644 index 00000000..7739bf54 --- /dev/null +++ b/components/approval/ApprovalPreviewDialog.tsx @@ -0,0 +1,211 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Loader2, FileText, AlertCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Separator } from "@/components/ui/separator"; + +import ApprovalLineSelector, { + type ApprovalLineItem, +} from "@/components/knox/approval/ApprovalLineSelector"; +import { getApprovalTemplateByName, replaceTemplateVariables } from "@/lib/approval/template-utils"; +import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils"; + +interface ApprovalPreviewDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + templateName: string; + variables: Record<string, string>; + title: string; + description?: string; + currentUser: { id: number; epId: string | null; name?: string | null; email?: string }; + onSubmit: (approvers: ApprovalLineItem[]) => Promise<void>; +} + +export function ApprovalPreviewDialog({ + open, + onOpenChange, + templateName, + variables, + title, + description, + currentUser, + onSubmit, +}: ApprovalPreviewDialogProps) { + const [approvalLines, setApprovalLines] = React.useState<ApprovalLineItem[]>([]); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [templateContent, setTemplateContent] = React.useState<string>(""); + const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false); + const [templateError, setTemplateError] = React.useState<string | null>(null); + + // 상신자 초기화 + React.useEffect(() => { + if (open && currentUser.epId) { + const drafterLine: ApprovalLineItem = { + id: `drafter-${Date.now()}`, + epId: currentUser.epId, + userId: currentUser.id.toString(), + emailAddress: currentUser.email, + name: currentUser.name || undefined, + role: "0", // 기안 + seq: "0", + opinion: "", + }; + setApprovalLines([drafterLine]); + } + }, [open, currentUser]); + + // 템플릿 로드 및 변수 치환 + React.useEffect(() => { + if (!open) return; + + const loadTemplate = async () => { + setIsLoadingTemplate(true); + setTemplateError(null); + + try { + const template = await getApprovalTemplateByName(templateName); + + if (!template) { + setTemplateError(`템플릿 "${templateName}"을 찾을 수 없습니다.`); + setTemplateContent(`<p class="text-gray-500">${description || "결재 요청"}</p>`); + } else { + // 변수 치환 + const replaced = await replaceTemplateVariables(template.content, variables); + setTemplateContent(replaced); + } + } catch (error) { + console.error("Template load error:", error); + setTemplateError("템플릿을 불러오는 중 오류가 발생했습니다."); + setTemplateContent(`<p class="text-gray-500">${description || "결재 요청"}</p>`); + } finally { + setIsLoadingTemplate(false); + } + }; + + loadTemplate(); + }, [open, templateName, variables, description]); + + const handleSubmit = async () => { + debugLog('[ApprovalPreviewDialog] 결재 제출 시작', { + templateName, + approvalLineCount: approvalLines.length, + }); + + // 결재자가 있는지 확인 (상신자 제외) + const approvers = approvalLines.filter((line) => line.seq !== "0"); + if (approvers.length === 0) { + debugError('[ApprovalPreviewDialog] 결재자가 없음'); + toast.error("결재자를 최소 1명 이상 추가해주세요."); + return; + } + + setIsSubmitting(true); + try { + debugLog('[ApprovalPreviewDialog] onSubmit 호출', { + approversCount: approvers.length, + }); + + await onSubmit(approvalLines); + + debugSuccess('[ApprovalPreviewDialog] 결재 요청 성공'); + toast.success("결재가 성공적으로 요청되었습니다."); + onOpenChange(false); + } catch (error) { + debugError('[ApprovalPreviewDialog] 결재 요청 실패', error); + const errorMessage = error instanceof Error ? error.message : "결재 요청에 실패했습니다."; + toast.error(errorMessage); + // 에러 발생 시 dialog를 닫지 않음 + } finally { + setIsSubmitting(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <FileText className="w-5 h-5" /> + {title} + </DialogTitle> + {description && <DialogDescription>{description}</DialogDescription>} + </DialogHeader> + + <div className="space-y-6"> + {/* 템플릿 미리보기 */} + <Card> + <CardHeader> + <CardTitle>결재 내용 미리보기</CardTitle> + <CardDescription> + 템플릿: <span className="font-mono text-sm">{templateName}</span> + </CardDescription> + </CardHeader> + <CardContent> + {isLoadingTemplate ? ( + <div className="flex items-center justify-center py-8"> + <Loader2 className="w-6 h-6 animate-spin text-gray-400" /> + <span className="ml-2 text-gray-500">템플릿을 불러오는 중...</span> + </div> + ) : templateError ? ( + <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg"> + <div className="flex items-center gap-2 text-yellow-700"> + <AlertCircle className="w-4 h-4" /> + <span className="font-medium">경고</span> + </div> + <p className="text-sm text-yellow-600 mt-1">{templateError}</p> + <p className="text-xs text-yellow-500 mt-2"> + 기본 내용으로 대체되었습니다. 결재는 정상적으로 진행됩니다. + </p> + </div> + ) : null} + + <div + className="border rounded-lg p-4 min-h-[200px] [&_table]:w-full [&_table]:border-collapse [&_table]:border [&_table]:border-border [&_th]:border [&_th]:border-border [&_th]:bg-muted/50 [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:font-semibold [&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_tr:hover]:bg-muted/30" + dangerouslySetInnerHTML={{ __html: templateContent }} + /> + </CardContent> + </Card> + + <Separator /> + + {/* 결재선 선택 */} + <div> + <ApprovalLineSelector + value={approvalLines} + onChange={setApprovalLines} + placeholder="결재자를 검색하세요..." + maxSelections={10} + /> + </div> + </div> + + <DialogFooter className="gap-2"> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button type="button" onClick={handleSubmit} disabled={isSubmitting || isLoadingTemplate}> + {isSubmitting && <Loader2 className="mr-2 w-4 h-4 animate-spin" />} + 결재 요청 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + |
