메일 본문 내용 사용자 경험 개선

This commit is contained in:
kjs
2025-12-09 13:50:17 +09:00
parent 1ee1287b8a
commit ece7f21bd3
6 changed files with 1401 additions and 30 deletions

View File

@@ -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>
{/* 옵션 탭 */}