메일 본문 내용 사용자 경험 개선
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
/**
|
||||
* 메일 발송 노드 속성 편집
|
||||
* - 메일관리에서 등록한 계정을 선택하여 발송
|
||||
* - 변수 태그 에디터로 본문 편집
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -18,6 +19,7 @@ import { Plus, Trash2, Mail, Server, FileText, Settings, RefreshCw, CheckCircle,
|
||||
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
||||
import { getMailAccounts, type MailAccount } from "@/lib/api/mail";
|
||||
import type { EmailActionNodeData } from "@/types/node-editor";
|
||||
import { VariableTagEditor, type VariableInfo } from "../../editors/VariableTagEditor";
|
||||
|
||||
interface EmailActionPropertiesProps {
|
||||
nodeId: string;
|
||||
@@ -25,13 +27,92 @@ interface EmailActionPropertiesProps {
|
||||
}
|
||||
|
||||
export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesProps) {
|
||||
const { updateNode } = useFlowEditorStore();
|
||||
const { updateNode, nodes, edges } = useFlowEditorStore();
|
||||
|
||||
// 메일 계정 목록
|
||||
const [mailAccounts, setMailAccounts] = useState<MailAccount[]>([]);
|
||||
const [isLoadingAccounts, setIsLoadingAccounts] = useState(false);
|
||||
const [accountError, setAccountError] = useState<string | null>(null);
|
||||
|
||||
// 🆕 플로우에서 사용 가능한 변수 목록 계산
|
||||
const availableVariables = useMemo<VariableInfo[]>(() => {
|
||||
const variables: VariableInfo[] = [];
|
||||
|
||||
// 기본 시스템 변수
|
||||
variables.push(
|
||||
{ name: "timestamp", displayName: "현재 시간", description: "메일 발송 시점의 타임스탬프" },
|
||||
{ name: "sourceData", displayName: "소스 데이터", description: "전체 소스 데이터 (JSON)" }
|
||||
);
|
||||
|
||||
// 현재 노드에 연결된 소스 노드들에서 필드 정보 수집
|
||||
const incomingEdges = edges.filter((e) => e.target === nodeId);
|
||||
|
||||
for (const edge of incomingEdges) {
|
||||
const sourceNode = nodes.find((n) => n.id === edge.source);
|
||||
if (!sourceNode) continue;
|
||||
|
||||
const nodeData = sourceNode.data as any;
|
||||
|
||||
// 테이블 소스 노드인 경우
|
||||
if (sourceNode.type === "tableSource" && nodeData.fields) {
|
||||
const tableName = nodeData.tableName || "테이블";
|
||||
nodeData.fields.forEach((field: any) => {
|
||||
variables.push({
|
||||
name: field.name,
|
||||
displayName: field.displayName || field.label || field.name,
|
||||
type: field.type,
|
||||
description: `${tableName} 테이블의 필드`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 외부 DB 소스 노드인 경우
|
||||
if (sourceNode.type === "externalDBSource" && nodeData.fields) {
|
||||
const tableName = nodeData.tableName || "외부 테이블";
|
||||
nodeData.fields.forEach((field: any) => {
|
||||
variables.push({
|
||||
name: field.name,
|
||||
displayName: field.displayName || field.label || field.name,
|
||||
type: field.type,
|
||||
description: `${tableName} (외부 DB) 필드`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// REST API 소스 노드인 경우
|
||||
if (sourceNode.type === "restAPISource" && nodeData.responseFields) {
|
||||
nodeData.responseFields.forEach((field: any) => {
|
||||
variables.push({
|
||||
name: field.name,
|
||||
displayName: field.displayName || field.label || field.name,
|
||||
type: field.type,
|
||||
description: "REST API 응답 필드",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 변환 노드인 경우 - 출력 필드 추가
|
||||
if (sourceNode.type === "dataTransform" && nodeData.transformations) {
|
||||
nodeData.transformations.forEach((transform: any) => {
|
||||
if (transform.targetField) {
|
||||
variables.push({
|
||||
name: transform.targetField,
|
||||
displayName: transform.targetField,
|
||||
description: "데이터 변환 결과 필드",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 제거
|
||||
const uniqueVariables = variables.filter(
|
||||
(v, index, self) => index === self.findIndex((t) => t.name === v.name)
|
||||
);
|
||||
|
||||
return uniqueVariables;
|
||||
}, [nodes, edges, nodeId]);
|
||||
|
||||
// 로컬 상태
|
||||
const [displayName, setDisplayName] = useState(data.displayName || "메일 발송");
|
||||
|
||||
@@ -473,31 +554,71 @@ export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesPro
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">텍스트</SelectItem>
|
||||
<SelectItem value="html">HTML</SelectItem>
|
||||
<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"
|
||||
/>
|
||||
|
||||
{/* 텍스트 형식: 변수 태그 에디터 사용 */}
|
||||
{bodyType === "text" && (
|
||||
<VariableTagEditor
|
||||
value={body}
|
||||
onChange={(newBody) => {
|
||||
setBody(newBody);
|
||||
updateNodeData({ body: newBody });
|
||||
}}
|
||||
variables={availableVariables}
|
||||
placeholder="메일 본문을 입력하세요. @ 또는 / 키로 변수를 삽입할 수 있습니다."
|
||||
minHeight="200px"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* HTML 형식: 직접 입력 */}
|
||||
{bodyType === "html" && (
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onBlur={updateMailContent}
|
||||
placeholder="<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>
|
||||
{/* 변수 안내 (HTML 모드에서만 표시) */}
|
||||
{bodyType === "html" && (
|
||||
<Card className="bg-gray-50">
|
||||
<CardContent className="p-3 text-xs text-gray-600">
|
||||
<div className="font-medium mb-1">사용 가능한 템플릿 변수:</div>
|
||||
{availableVariables.slice(0, 5).map((v) => (
|
||||
<code key={v.name} className="block">
|
||||
{`{{${v.name}}}`} - {v.displayName}
|
||||
</code>
|
||||
))}
|
||||
{availableVariables.length > 5 && (
|
||||
<span className="text-gray-400">...외 {availableVariables.length - 5}개</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 변수 태그 에디터 안내 (텍스트 모드에서만 표시) */}
|
||||
{bodyType === "text" && (
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<CardContent className="p-3 text-xs text-blue-700">
|
||||
<div className="font-medium mb-1">변수 삽입 방법:</div>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
<li><kbd className="bg-blue-100 px-1 rounded">@</kbd> 또는 <kbd className="bg-blue-100 px-1 rounded">/</kbd> 키를 눌러 변수 목록 열기</li>
|
||||
<li>툴바의 "변수 삽입" 버튼 클릭</li>
|
||||
<li>삽입된 변수는 파란색 태그로 표시됩니다</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 옵션 탭 */}
|
||||
|
||||
Reference in New Issue
Block a user