외부호출 노드들
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 메일 발송 노드 속성 편집
|
||||
* - 메일관리에서 등록한 계정을 선택하여 발송
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, Trash2, Mail, Server, FileText, Settings, RefreshCw, CheckCircle, AlertCircle, User } from "lucide-react";
|
||||
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||||
import { getMailAccounts, type MailAccount } from "@/lib/api/mail";
|
||||
import type { EmailActionNodeData } from "@/types/node-editor";
|
||||
|
||||
interface EmailActionPropertiesProps {
|
||||
nodeId: string;
|
||||
data: EmailActionNodeData;
|
||||
}
|
||||
|
||||
export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesProps) {
|
||||
const { updateNode } = useFlowEditorStore();
|
||||
|
||||
// 메일 계정 목록
|
||||
const [mailAccounts, setMailAccounts] = useState<MailAccount[]>([]);
|
||||
const [isLoadingAccounts, setIsLoadingAccounts] = useState(false);
|
||||
const [accountError, setAccountError] = useState<string | null>(null);
|
||||
|
||||
// 로컬 상태
|
||||
const [displayName, setDisplayName] = useState(data.displayName || "메일 발송");
|
||||
|
||||
// 계정 선택
|
||||
const [selectedAccountId, setSelectedAccountId] = useState(data.accountId || "");
|
||||
|
||||
// 메일 내용
|
||||
const [to, setTo] = useState(data.to || "");
|
||||
const [cc, setCc] = useState(data.cc || "");
|
||||
const [bcc, setBcc] = useState(data.bcc || "");
|
||||
const [subject, setSubject] = useState(data.subject || "");
|
||||
const [body, setBody] = useState(data.body || "");
|
||||
const [bodyType, setBodyType] = useState<"text" | "html">(data.bodyType || "text");
|
||||
|
||||
// 고급 설정
|
||||
const [replyTo, setReplyTo] = useState(data.replyTo || "");
|
||||
const [priority, setPriority] = useState<"high" | "normal" | "low">(data.priority || "normal");
|
||||
const [timeout, setTimeout] = useState(data.options?.timeout?.toString() || "30000");
|
||||
const [retryCount, setRetryCount] = useState(data.options?.retryCount?.toString() || "3");
|
||||
|
||||
// 메일 계정 목록 로드
|
||||
const loadMailAccounts = useCallback(async () => {
|
||||
setIsLoadingAccounts(true);
|
||||
setAccountError(null);
|
||||
try {
|
||||
const accounts = await getMailAccounts();
|
||||
setMailAccounts(accounts.filter(acc => acc.status === 'active'));
|
||||
} catch (error) {
|
||||
console.error("메일 계정 로드 실패:", error);
|
||||
setAccountError("메일 계정을 불러오는데 실패했습니다");
|
||||
} finally {
|
||||
setIsLoadingAccounts(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 마운트 시 메일 계정 로드
|
||||
useEffect(() => {
|
||||
loadMailAccounts();
|
||||
}, [loadMailAccounts]);
|
||||
|
||||
// 데이터 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
setDisplayName(data.displayName || "메일 발송");
|
||||
setSelectedAccountId(data.accountId || "");
|
||||
setTo(data.to || "");
|
||||
setCc(data.cc || "");
|
||||
setBcc(data.bcc || "");
|
||||
setSubject(data.subject || "");
|
||||
setBody(data.body || "");
|
||||
setBodyType(data.bodyType || "text");
|
||||
setReplyTo(data.replyTo || "");
|
||||
setPriority(data.priority || "normal");
|
||||
setTimeout(data.options?.timeout?.toString() || "30000");
|
||||
setRetryCount(data.options?.retryCount?.toString() || "3");
|
||||
}, [data]);
|
||||
|
||||
// 선택된 계정 정보
|
||||
const selectedAccount = mailAccounts.find(acc => acc.id === selectedAccountId);
|
||||
|
||||
// 노드 업데이트 함수
|
||||
const updateNodeData = useCallback(
|
||||
(updates: Partial<EmailActionNodeData>) => {
|
||||
updateNode(nodeId, {
|
||||
...data,
|
||||
...updates,
|
||||
});
|
||||
},
|
||||
[nodeId, data, updateNode]
|
||||
);
|
||||
|
||||
// 표시명 변경
|
||||
const handleDisplayNameChange = (value: string) => {
|
||||
setDisplayName(value);
|
||||
updateNodeData({ displayName: value });
|
||||
};
|
||||
|
||||
// 계정 선택 변경
|
||||
const handleAccountChange = (accountId: string) => {
|
||||
setSelectedAccountId(accountId);
|
||||
const account = mailAccounts.find(acc => acc.id === accountId);
|
||||
updateNodeData({
|
||||
accountId,
|
||||
// 계정의 이메일을 발신자로 자동 설정
|
||||
from: account?.email || ""
|
||||
});
|
||||
};
|
||||
|
||||
// 메일 내용 업데이트
|
||||
const updateMailContent = useCallback(() => {
|
||||
updateNodeData({
|
||||
to,
|
||||
cc: cc || undefined,
|
||||
bcc: bcc || undefined,
|
||||
subject,
|
||||
body,
|
||||
bodyType,
|
||||
replyTo: replyTo || undefined,
|
||||
priority,
|
||||
});
|
||||
}, [to, cc, bcc, subject, body, bodyType, replyTo, priority, updateNodeData]);
|
||||
|
||||
// 옵션 업데이트
|
||||
const updateOptions = useCallback(() => {
|
||||
updateNodeData({
|
||||
options: {
|
||||
timeout: parseInt(timeout) || 30000,
|
||||
retryCount: parseInt(retryCount) || 3,
|
||||
},
|
||||
});
|
||||
}, [timeout, retryCount, updateNodeData]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
{/* 표시명 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">표시명</Label>
|
||||
<Input
|
||||
value={displayName}
|
||||
onChange={(e) => handleDisplayNameChange(e.target.value)}
|
||||
placeholder="메일 발송"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="account" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="account" className="text-xs">
|
||||
<User className="mr-1 h-3 w-3" />
|
||||
계정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mail" className="text-xs">
|
||||
<Mail className="mr-1 h-3 w-3" />
|
||||
메일
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="content" className="text-xs">
|
||||
<FileText className="mr-1 h-3 w-3" />
|
||||
본문
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="options" className="text-xs">
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
옵션
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 계정 선택 탭 */}
|
||||
<TabsContent value="account" className="space-y-3 pt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">발송 계정 선택 *</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={loadMailAccounts}
|
||||
disabled={isLoadingAccounts}
|
||||
className="h-6 px-2"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 ${isLoadingAccounts ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{accountError && (
|
||||
<div className="flex items-center gap-2 text-xs text-red-500">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
{accountError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value={selectedAccountId}
|
||||
onValueChange={handleAccountChange}
|
||||
disabled={isLoadingAccounts}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder={isLoadingAccounts ? "로딩 중..." : "메일 계정을 선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mailAccounts.length === 0 ? (
|
||||
<div className="p-2 text-xs text-gray-500">
|
||||
등록된 메일 계정이 없습니다.
|
||||
<br />
|
||||
관리자 > 메일관리 > 계정관리에서 추가하세요.
|
||||
</div>
|
||||
) : (
|
||||
mailAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={account.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{account.name}</span>
|
||||
<span className="text-xs text-gray-500">({account.email})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 선택된 계정 정보 표시 */}
|
||||
{selectedAccount && (
|
||||
<Card className="bg-green-50 border-green-200">
|
||||
<CardContent className="p-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-green-700">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">선택된 계정</span>
|
||||
</div>
|
||||
<div className="text-xs space-y-1 text-green-800">
|
||||
<div><strong>이름:</strong> {selectedAccount.name}</div>
|
||||
<div><strong>이메일:</strong> {selectedAccount.email}</div>
|
||||
<div><strong>SMTP:</strong> {selectedAccount.smtpHost}:{selectedAccount.smtpPort}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!selectedAccount && mailAccounts.length > 0 && (
|
||||
<Card className="bg-yellow-50 border-yellow-200">
|
||||
<CardContent className="p-3 text-xs text-yellow-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>메일 발송을 위해 계정을 선택해주세요.</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{mailAccounts.length === 0 && !isLoadingAccounts && (
|
||||
<Card className="bg-gray-50">
|
||||
<CardContent className="p-3 text-xs text-gray-600">
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">메일 계정 등록 방법:</div>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>관리자 메뉴로 이동</li>
|
||||
<li>메일관리 > 계정관리 선택</li>
|
||||
<li>새 계정 추가 버튼 클릭</li>
|
||||
<li>SMTP 정보 입력 후 저장</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 메일 설정 탭 */}
|
||||
<TabsContent value="mail" className="space-y-3 pt-3">
|
||||
{/* 발신자는 선택된 계정에서 자동으로 설정됨 */}
|
||||
{selectedAccount && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">발신자 (From)</Label>
|
||||
<div className="h-8 px-3 py-2 text-sm bg-gray-100 rounded-md border flex items-center">
|
||||
{selectedAccount.email}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">선택한 계정의 이메일 주소가 자동으로 사용됩니다.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">수신자 (To) *</Label>
|
||||
<Input
|
||||
value={to}
|
||||
onChange={(e) => setTo(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="recipient@example.com (쉼표로 구분)"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">참조 (CC)</Label>
|
||||
<Input
|
||||
value={cc}
|
||||
onChange={(e) => setCc(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="cc@example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">숨은 참조 (BCC)</Label>
|
||||
<Input
|
||||
value={bcc}
|
||||
onChange={(e) => setBcc(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="bcc@example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">회신 주소 (Reply-To)</Label>
|
||||
<Input
|
||||
value={replyTo}
|
||||
onChange={(e) => setReplyTo(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="reply@example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">우선순위</Label>
|
||||
<Select value={priority} onValueChange={(v: "high" | "normal" | "low") => {
|
||||
setPriority(v);
|
||||
updateNodeData({ priority: v });
|
||||
}}>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="high">높음</SelectItem>
|
||||
<SelectItem value="normal">보통</SelectItem>
|
||||
<SelectItem value="low">낮음</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 본문 탭 */}
|
||||
<TabsContent value="content" className="space-y-3 pt-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">제목 *</Label>
|
||||
<Input
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="메일 제목 ({{변수}} 사용 가능)"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">본문 형식</Label>
|
||||
<Select value={bodyType} onValueChange={(v: "text" | "html") => {
|
||||
setBodyType(v);
|
||||
updateNodeData({ bodyType: v });
|
||||
}}>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">텍스트</SelectItem>
|
||||
<SelectItem value="html">HTML</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">본문 내용</Label>
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder={bodyType === "html" ? "<html><body>...</body></html>" : "메일 본문 내용"}
|
||||
className="min-h-[200px] text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gray-50">
|
||||
<CardContent className="p-3 text-xs text-gray-600">
|
||||
<div className="font-medium mb-1">사용 가능한 템플릿 변수:</div>
|
||||
<code className="block">{"{{sourceData}}"}</code>
|
||||
<code className="block">{"{{timestamp}}"}</code>
|
||||
<code className="block">{"{{필드명}}"}</code>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 옵션 탭 */}
|
||||
<TabsContent value="options" className="space-y-3 pt-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">타임아웃 (ms)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout(e.target.value)}
|
||||
onBlur={updateOptions}
|
||||
placeholder="30000"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">재시도 횟수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={retryCount}
|
||||
onChange={(e) => setRetryCount(e.target.value)}
|
||||
onBlur={updateOptions}
|
||||
placeholder="3"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user