diff --git a/frontend/components/dataflow/node-editor/panels/properties/EmailActionProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/EmailActionProperties.tsx
index b57ba029..211dc6db 100644
--- a/frontend/components/dataflow/node-editor/panels/properties/EmailActionProperties.tsx
+++ b/frontend/components/dataflow/node-editor/panels/properties/EmailActionProperties.tsx
@@ -38,6 +38,11 @@ export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesPro
// ๊ณ์ ์ ํ
const [selectedAccountId, setSelectedAccountId] = useState(data.accountId || "");
+ // ๐ ์์ ์ ์ปดํฌ๋ํธ ์ฌ์ฉ ์ฌ๋ถ
+ const [useRecipientComponent, setUseRecipientComponent] = useState(data.useRecipientComponent ?? false);
+ const [recipientToField, setRecipientToField] = useState(data.recipientToField || "mailTo");
+ const [recipientCcField, setRecipientCcField] = useState(data.recipientCcField || "mailCc");
+
// ๋ฉ์ผ ๋ด์ฉ
const [to, setTo] = useState(data.to || "");
const [cc, setCc] = useState(data.cc || "");
@@ -76,6 +81,9 @@ export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesPro
useEffect(() => {
setDisplayName(data.displayName || "๋ฉ์ผ ๋ฐ์ก");
setSelectedAccountId(data.accountId || "");
+ setUseRecipientComponent(data.useRecipientComponent ?? false);
+ setRecipientToField(data.recipientToField || "mailTo");
+ setRecipientCcField(data.recipientCcField || "mailCc");
setTo(data.to || "");
setCc(data.cc || "");
setBcc(data.bcc || "");
@@ -286,27 +294,121 @@ export function EmailActionProperties({ nodeId, data }: EmailActionPropertiesPro
)}
-
-
- setTo(e.target.value)}
- onBlur={updateMailContent}
- placeholder="recipient@example.com (์ผํ๋ก ๊ตฌ๋ถ)"
- className="h-8 text-sm"
- />
-
+ {/* ๐ ์์ ์ ์ปดํฌ๋ํธ ์ฌ์ฉ ์ต์
*/}
+
+
+
+
+
+
+ ํ๋ฉด์ ๋ฐฐ์นํ "๋ฉ์ผ ์์ ์ ์ ํ" ์ปดํฌ๋ํธ์ ๊ฐ์ ์๋์ผ๋ก ์ฌ์ฉํฉ๋๋ค.
+
+
+
{
+ setUseRecipientComponent(checked);
+ if (checked) {
+ // ์ฒดํฌ ์ ์๋์ผ๋ก ๋ณ์ ์ค์
+ updateNodeData({
+ useRecipientComponent: true,
+ recipientToField,
+ recipientCcField,
+ to: `{{${recipientToField}}}`,
+ cc: `{{${recipientCcField}}}`,
+ });
+ setTo(`{{${recipientToField}}}`);
+ setCc(`{{${recipientCcField}}}`);
+ } else {
+ updateNodeData({
+ useRecipientComponent: false,
+ to: "",
+ cc: "",
+ });
+ setTo("");
+ setCc("");
+ }
+ }}
+ />
+
+
+ {/* ํ๋๋ช
์ค์ (์์ ์ ์ปดํฌ๋ํธ ์ฌ์ฉ ์) */}
+ {useRecipientComponent && (
+
+
+
+ ์๋ ์ค์ ๋จ:
+
+ ์์ ์: {`{{${recipientToField}}}`}
+
+ ์ฐธ์กฐ: {`{{${recipientCcField}}}`}
+
+
+ )}
+
+
-
-
- setCc(e.target.value)}
- onBlur={updateMailContent}
- placeholder="cc@example.com"
- className="h-8 text-sm"
- />
-
+ {/* ์์ ์ ์ง์ ์
๋ ฅ (์ปดํฌ๋ํธ ๋ฏธ์ฌ์ฉ ์) */}
+ {!useRecipientComponent && (
+ <>
+
+
+ setTo(e.target.value)}
+ onBlur={updateMailContent}
+ placeholder="recipient@example.com (์ผํ๋ก ๊ตฌ๋ถ, {{๋ณ์}} ์ฌ์ฉ ๊ฐ๋ฅ)"
+ className="h-8 text-sm"
+ />
+
+
+
+
+ setCc(e.target.value)}
+ onBlur={updateMailContent}
+ placeholder="cc@example.com"
+ className="h-8 text-sm"
+ />
+
+ >
+ )}
diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts
index 4babf09d..98f2522f 100644
--- a/frontend/lib/registry/components/index.ts
+++ b/frontend/lib/registry/components/index.ts
@@ -83,6 +83,9 @@ import "./rack-structure/RackStructureRenderer"; // ์ฐฝ๊ณ ๋ ์์น ์ผ๊ด ์
// ๐ ์ธ๊ธ๊ณ์ฐ์ ๊ด๋ฆฌ ์ปดํฌ๋ํธ
import "./tax-invoice-list/TaxInvoiceListRenderer"; // ์ธ๊ธ๊ณ์ฐ์ ๋ชฉ๋ก, ์์ฑ, ๋ฐํ, ์ทจ์
+// ๐ ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ
+import "./mail-recipient-selector/MailRecipientSelectorRenderer"; // ๋ด๋ถ ์ธ์ ์ ํ + ์ธ๋ถ ์ด๋ฉ์ผ ์
๋ ฅ
+
/**
* ์ปดํฌ๋ํธ ์ด๊ธฐํ ํจ์
*/
diff --git a/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorComponent.tsx b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorComponent.tsx
new file mode 100644
index 00000000..57102413
--- /dev/null
+++ b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorComponent.tsx
@@ -0,0 +1,458 @@
+"use client";
+
+/**
+ * ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ
+ * InteractiveScreenViewer์์ ์ฌ์ฉํ๋ ๋ํผ ์ปดํฌ๋ํธ
+ */
+
+import React, { useState, useEffect, useCallback } from "react";
+import { X, Plus, Users, Mail, Check } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Label } from "@/components/ui/label";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { cn } from "@/lib/utils";
+import { getUserList } from "@/lib/api/user";
+import type {
+ MailRecipientSelectorConfig,
+ Recipient,
+ InternalUser,
+} from "./types";
+
+interface MailRecipientSelectorComponentProps {
+ // ์ปดํฌ๋ํธ ๊ธฐ๋ณธ Props
+ id?: string;
+ componentConfig?: MailRecipientSelectorConfig;
+
+ // ํผ ๋ฐ์ดํฐ ์ฐ๋
+ formData?: Record
;
+ onFormDataChange?: (fieldName: string, value: any) => void;
+
+ // ์คํ์ผ
+ className?: string;
+ style?: React.CSSProperties;
+
+ // ๋ชจ๋
+ isPreviewMode?: boolean;
+ isInteractive?: boolean;
+ isDesignMode?: boolean;
+
+ // ๊ธฐํ Props (๋ฌด์)
+ [key: string]: any;
+}
+
+export const MailRecipientSelectorComponent: React.FC<
+ MailRecipientSelectorComponentProps
+> = ({
+ id,
+ componentConfig,
+ formData = {},
+ onFormDataChange,
+ className,
+ style,
+ isPreviewMode = false,
+ isInteractive = true,
+ isDesignMode = false,
+ ...rest
+}) => {
+ // config ๊ธฐ๋ณธ๊ฐ
+ const config = componentConfig || {};
+ const {
+ toFieldName = "mailTo",
+ ccFieldName = "mailCc",
+ showCc = true,
+ showInternalSelector = true,
+ showExternalInput = true,
+ toLabel = "์์ ์",
+ ccLabel = "์ฐธ์กฐ(CC)",
+ maxRecipients,
+ maxCcRecipients,
+ required = true,
+ } = config;
+
+ // ์ํ
+ const [toRecipients, setToRecipients] = useState([]);
+ const [ccRecipients, setCcRecipients] = useState([]);
+ const [externalEmail, setExternalEmail] = useState("");
+ const [externalCcEmail, setExternalCcEmail] = useState("");
+ const [internalUsers, setInternalUsers] = useState([]);
+ const [isLoadingUsers, setIsLoadingUsers] = useState(false);
+ const [toPopoverOpen, setToPopoverOpen] = useState(false);
+ const [ccPopoverOpen, setCcPopoverOpen] = useState(false);
+
+ // ๋ด๋ถ ์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋
+ const loadInternalUsers = useCallback(async () => {
+ if (!showInternalSelector || isDesignMode) return;
+
+ setIsLoadingUsers(true);
+ try {
+ const response = await getUserList({ status: "active", limit: 1000 });
+ if (response.success && response.data) {
+ setInternalUsers(response.data);
+ }
+ } catch (error) {
+ console.error("์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋ ์คํจ:", error);
+ } finally {
+ setIsLoadingUsers(false);
+ }
+ }, [showInternalSelector, isDesignMode]);
+
+ // ์ปดํฌ๋ํธ ๋ง์ดํธ ์ ์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋
+ useEffect(() => {
+ loadInternalUsers();
+ }, [loadInternalUsers]);
+
+ // formData์์ ์ด๊ธฐ๊ฐ ๋ก๋
+ useEffect(() => {
+ if (formData[toFieldName]) {
+ const emails = formData[toFieldName].split(",").filter(Boolean);
+ const recipients: Recipient[] = emails.map((email: string) => ({
+ id: `external-${email.trim()}`,
+ email: email.trim(),
+ type: "external" as const,
+ }));
+ setToRecipients(recipients);
+ }
+
+ if (formData[ccFieldName]) {
+ const emails = formData[ccFieldName].split(",").filter(Boolean);
+ const recipients: Recipient[] = emails.map((email: string) => ({
+ id: `external-${email.trim()}`,
+ email: email.trim(),
+ type: "external" as const,
+ }));
+ setCcRecipients(recipients);
+ }
+ }, []);
+
+ // ์์ ์ ๋ณ๊ฒฝ ์ formData ์
๋ฐ์ดํธ
+ const updateFormData = useCallback(
+ (recipients: Recipient[], fieldName: string) => {
+ const emailString = recipients.map((r) => r.email).join(",");
+ onFormDataChange?.(fieldName, emailString);
+ },
+ [onFormDataChange]
+ );
+
+ // ์์ ์ ์ถ๊ฐ (๋ด๋ถ ์ฌ์ฉ์)
+ const addInternalRecipient = useCallback(
+ (user: InternalUser, type: "to" | "cc") => {
+ const email = user.email || `${user.userId}@company.com`;
+ const newRecipient: Recipient = {
+ id: `internal-${user.userId}`,
+ email,
+ name: user.userName,
+ type: "internal",
+ userId: user.userId,
+ };
+
+ if (type === "to") {
+ // ์ค๋ณต ์ฒดํฌ
+ if (toRecipients.some((r) => r.email === email)) return;
+ // ์ต๋ ์์ ์ ์ ์ฒดํฌ
+ if (maxRecipients && toRecipients.length >= maxRecipients) return;
+
+ const updated = [...toRecipients, newRecipient];
+ setToRecipients(updated);
+ updateFormData(updated, toFieldName);
+ setToPopoverOpen(false);
+ } else {
+ if (ccRecipients.some((r) => r.email === email)) return;
+ if (maxCcRecipients && ccRecipients.length >= maxCcRecipients) return;
+
+ const updated = [...ccRecipients, newRecipient];
+ setCcRecipients(updated);
+ updateFormData(updated, ccFieldName);
+ setCcPopoverOpen(false);
+ }
+ },
+ [
+ toRecipients,
+ ccRecipients,
+ maxRecipients,
+ maxCcRecipients,
+ toFieldName,
+ ccFieldName,
+ updateFormData,
+ ]
+ );
+
+ // ์ธ๋ถ ์ด๋ฉ์ผ ์ถ๊ฐ
+ const addExternalEmail = useCallback(
+ (email: string, type: "to" | "cc") => {
+ // ์ด๋ฉ์ผ ํ์ ๊ฒ์ฆ
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) return;
+
+ const newRecipient: Recipient = {
+ id: `external-${email}-${type}`,
+ email,
+ type: "external",
+ };
+
+ if (type === "to") {
+ // ํด๋น ํ๋ ๋ด์์๋ง ์ค๋ณต ์ฒดํฌ
+ if (toRecipients.some((r) => r.email === email)) return;
+ if (maxRecipients && toRecipients.length >= maxRecipients) return;
+
+ const updated = [...toRecipients, newRecipient];
+ setToRecipients(updated);
+ updateFormData(updated, toFieldName);
+ setExternalEmail("");
+ } else {
+ // ํด๋น ํ๋ ๋ด์์๋ง ์ค๋ณต ์ฒดํฌ
+ if (ccRecipients.some((r) => r.email === email)) return;
+ if (maxCcRecipients && ccRecipients.length >= maxCcRecipients) return;
+
+ const updated = [...ccRecipients, newRecipient];
+ setCcRecipients(updated);
+ updateFormData(updated, ccFieldName);
+ setExternalCcEmail("");
+ }
+ },
+ [
+ toRecipients,
+ ccRecipients,
+ maxRecipients,
+ maxCcRecipients,
+ toFieldName,
+ ccFieldName,
+ updateFormData,
+ ]
+ );
+
+ // ์์ ์ ์ ๊ฑฐ
+ const removeRecipient = useCallback(
+ (recipientId: string, type: "to" | "cc") => {
+ if (type === "to") {
+ const updated = toRecipients.filter((r) => r.id !== recipientId);
+ setToRecipients(updated);
+ updateFormData(updated, toFieldName);
+ } else {
+ const updated = ccRecipients.filter((r) => r.id !== recipientId);
+ setCcRecipients(updated);
+ updateFormData(updated, ccFieldName);
+ }
+ },
+ [toRecipients, ccRecipients, toFieldName, ccFieldName, updateFormData]
+ );
+
+ // ์ด๋ฏธ ์ ํ๋ ์ฌ์ฉ์์ธ์ง ํ์ธ (ํด๋น ํ๋ ๋ด์์๋ง ์ค๋ณต ์ฒดํฌ)
+ const isUserSelected = useCallback(
+ (user: InternalUser, type: "to" | "cc") => {
+ const recipients = type === "to" ? toRecipients : ccRecipients;
+ const userEmail = user.email || `${user.userId}@company.com`;
+ return recipients.some((r) => r.email === userEmail);
+ },
+ [toRecipients, ccRecipients]
+ );
+
+ // ์์ ์ ํ๊ทธ ๋ ๋๋ง
+ const renderRecipientTags = (recipients: Recipient[], type: "to" | "cc") => (
+
+ {recipients.map((recipient) => (
+
+ {recipient.type === "internal" ? (
+
+ ) : (
+
+ )}
+
+ {recipient.name || recipient.email}
+
+ {isInteractive && !isDesignMode && (
+
+ )}
+
+ ))}
+
+ );
+
+ // ๋ด๋ถ ์ฌ์ฉ์ ์ ํ ํ์ค๋ฒ
+ const renderInternalSelector = (type: "to" | "cc") => {
+ const isOpen = type === "to" ? toPopoverOpen : ccPopoverOpen;
+ const setIsOpen = type === "to" ? setToPopoverOpen : setCcPopoverOpen;
+
+ return (
+
+
+
+
+
+
+
+
+ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค.
+
+ {internalUsers.map((user, index) => {
+ const userEmail = user.email || `${user.userId}@company.com`;
+ const selected = isUserSelected(user, type);
+ const uniqueKey = `${user.userId}-${index}`;
+ return (
+ {
+ if (!selected) {
+ addInternalRecipient(user, type);
+ }
+ }}
+ className={cn(
+ "cursor-pointer",
+ selected && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+ {user.userName}
+
+ {userEmail}
+ {user.deptName && ` | ${user.deptName}`}
+
+
+ {selected &&
}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+ };
+
+ // ์ธ๋ถ ์ด๋ฉ์ผ ์
๋ ฅ
+ const renderExternalInput = (type: "to" | "cc") => {
+ const value = type === "to" ? externalEmail : externalCcEmail;
+ const setValue = type === "to" ? setExternalEmail : setExternalCcEmail;
+
+ return (
+
+
setValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ addExternalEmail(value, type);
+ }
+ }}
+ className="h-8 flex-1 text-sm"
+ disabled={!isInteractive || isDesignMode}
+ />
+
+
+ );
+ };
+
+ // ๋์์ธ ๋ชจ๋ ๋๋ ํ๋ฆฌ๋ทฐ ๋ชจ๋
+ if (isDesignMode || isPreviewMode) {
+ return (
+
+
๋ฉ์ผ ์์ ์ ์ ํ
+
+
+
+ ๋ด๋ถ ์ธ์ ์ ํ
+
+
+
+ ์ธ๋ถ ์ด๋ฉ์ผ ์
๋ ฅ
+
+
+
+ );
+ }
+
+ return (
+
+ {/* ์์ ์ (To) */}
+
+
+
+ {/* ์ ํ๋ ์์ ์ ํ๊ทธ */}
+ {toRecipients.length > 0 && (
+
+ {renderRecipientTags(toRecipients, "to")}
+
+ )}
+
+ {/* ์ถ๊ฐ ๋ฒํผ๋ค */}
+
+ {showInternalSelector && renderInternalSelector("to")}
+ {showExternalInput && renderExternalInput("to")}
+
+
+
+ {/* ์ฐธ์กฐ (CC) */}
+ {showCc && (
+
+
+
+ {/* ์ ํ๋ ์ฐธ์กฐ ์์ ์ ํ๊ทธ */}
+ {ccRecipients.length > 0 && (
+
+ {renderRecipientTags(ccRecipients, "cc")}
+
+ )}
+
+ {/* ์ถ๊ฐ ๋ฒํผ๋ค */}
+
+ {showInternalSelector && renderInternalSelector("cc")}
+ {showExternalInput && renderExternalInput("cc")}
+
+
+ )}
+
+ );
+};
+
+export default MailRecipientSelectorComponent;
diff --git a/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel.tsx b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel.tsx
new file mode 100644
index 00000000..1d42fb60
--- /dev/null
+++ b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel.tsx
@@ -0,0 +1,246 @@
+"use client";
+
+/**
+ * ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ ์ค์ ํจ๋
+ * ํ๋ฉด๊ด๋ฆฌ์์ ์ปดํฌ๋ํธ ์ค์ ์ ์ฌ์ฉ
+ */
+
+import React, { useEffect, useState, useCallback } from "react";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import type { MailRecipientSelectorConfig } from "./types";
+
+interface MailRecipientSelectorConfigPanelProps {
+ config: MailRecipientSelectorConfig;
+ onConfigChange: (config: MailRecipientSelectorConfig) => void;
+}
+
+export const MailRecipientSelectorConfigPanel: React.FC<
+ MailRecipientSelectorConfigPanelProps
+> = ({ config, onConfigChange }) => {
+ // ๋ก์ปฌ ์ํ
+ const [localConfig, setLocalConfig] = useState({
+ toFieldName: "mailTo",
+ ccFieldName: "mailCc",
+ showCc: true,
+ showInternalSelector: true,
+ showExternalInput: true,
+ toLabel: "์์ ์",
+ ccLabel: "์ฐธ์กฐ(CC)",
+ required: true,
+ ...config,
+ });
+
+ // config prop ๋ณ๊ฒฝ ์ ๋ก์ปฌ ์ํ ๋๊ธฐํ
+ useEffect(() => {
+ setLocalConfig({
+ toFieldName: "mailTo",
+ ccFieldName: "mailCc",
+ showCc: true,
+ showInternalSelector: true,
+ showExternalInput: true,
+ toLabel: "์์ ์",
+ ccLabel: "์ฐธ์กฐ(CC)",
+ required: true,
+ ...config,
+ });
+ }, [config]);
+
+ // ์ค์ ์
๋ฐ์ดํธ ํจ์
+ const updateConfig = useCallback(
+ (key: keyof MailRecipientSelectorConfig, value: any) => {
+ const newConfig = { ...localConfig, [key]: value };
+ setLocalConfig(newConfig);
+ onConfigChange(newConfig);
+ },
+ [localConfig, onConfigChange]
+ );
+
+ return (
+
+ {/* ํ๋๋ช
์ค์ */}
+
+
+ ํ๋๋ช
์ค์
+
+
+
+
+
updateConfig("toFieldName", e.target.value)}
+ placeholder="mailTo"
+ className="h-8 text-sm"
+ />
+
+ formData์ ์ ์ฅ๋ ์์ ์ ํ๋๋ช
(์ ์ด์์ {`{{mailTo}}`}๋ก ์ฌ์ฉ)
+
+
+
+
+
+
updateConfig("ccFieldName", e.target.value)}
+ placeholder="mailCc"
+ className="h-8 text-sm"
+ />
+
+ formData์ ์ ์ฅ๋ ์ฐธ์กฐ ํ๋๋ช
(์ ์ด์์ {`{{mailCc}}`}๋ก ์ฌ์ฉ)
+
+
+
+
+
+ {/* ํ์ ์ต์
*/}
+
+
+ ํ์ ์ต์
+
+
+
+
+
+
์ฐธ์กฐ ์์ ์ ์
๋ ฅ ํ๋ ํ์
+
+
updateConfig("showCc", checked)}
+ />
+
+
+
+
+
+
ํ์ฌ ๋ด๋ถ ์ฌ์ฉ์ ์ ํ ๊ธฐ๋ฅ
+
+
+ updateConfig("showInternalSelector", checked)
+ }
+ />
+
+
+
+
+
+
์ธ๋ถ ์ด๋ฉ์ผ ์ง์ ์
๋ ฅ ๊ธฐ๋ฅ
+
+
+ updateConfig("showExternalInput", checked)
+ }
+ />
+
+
+
+
+
+
์์ ์ ํ์ ์
๋ ฅ ์ฌ๋ถ
+
+
updateConfig("required", checked)}
+ />
+
+
+
+
+ {/* ๋ผ๋ฒจ ์ค์ */}
+
+
+ ๋ผ๋ฒจ ์ค์
+
+
+
+
+ updateConfig("toLabel", e.target.value)}
+ placeholder="์์ ์"
+ className="h-8 text-sm"
+ />
+
+
+
+
+ updateConfig("ccLabel", e.target.value)}
+ placeholder="์ฐธ์กฐ(CC)"
+ className="h-8 text-sm"
+ />
+
+
+
+
+ {/* ์ ํ ์ค์ */}
+
+
+ ์ ํ ์ค์
+
+
+
+
+
+ updateConfig(
+ "maxRecipients",
+ e.target.value ? parseInt(e.target.value) : undefined
+ )
+ }
+ placeholder="๋ฌด์ ํ"
+ className="h-8 text-sm"
+ />
+
+
+
+
+
+ updateConfig(
+ "maxCcRecipients",
+ e.target.value ? parseInt(e.target.value) : undefined
+ )
+ }
+ placeholder="๋ฌด์ ํ"
+ className="h-8 text-sm"
+ />
+
+
+
+
+ {/* ์ฌ์ฉ ์๋ด */}
+
+
+
+
์ฌ์ฉ ๋ฐฉ๋ฒ
+
+ - ์ด ์ปดํฌ๋ํธ๋ฅผ ๋ชจ๋ฌ ํ๋ฉด์ ๋ฐฐ์นํฉ๋๋ค.
+ -
+ ์ ์ด๊ด๋ฆฌ์ ๋ฉ์ผ ๋ฐ์ก ๋
ธ๋์์ ์์ ์๋ฅผ{" "}
+
{`{{mailTo}}`}๋ก ์ค์ ํฉ๋๋ค.
+
+ -
+ ์ฐธ์กฐ๋{" "}
+
{`{{mailCc}}`}๋ก ์ค์ ํฉ๋๋ค.
+
+ - ๋ถ๋ชจ ํ๋ฉด์์ ์ ํํ ๋ฐ์ดํฐ๋ ๋ณ๋ ๋ณ์๋ก ์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค.
+
+
+
+
+
+ );
+};
+
+export default MailRecipientSelectorConfigPanel;
+
diff --git a/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorRenderer.tsx b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorRenderer.tsx
new file mode 100644
index 00000000..388c62ce
--- /dev/null
+++ b/frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorRenderer.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+/**
+ * ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ ๋ ๋๋ฌ
+ * ComponentRegistry์ ์๋ ๋ฑ๋กํ๊ธฐ ์ํ ํด๋์ค
+ */
+
+import React from "react";
+import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
+import { MailRecipientSelectorDefinition } from "./index";
+import { MailRecipientSelectorComponent } from "./MailRecipientSelectorComponent";
+
+/**
+ * MailRecipientSelector ๋ ๋๋ฌ
+ * ์๋ ๋ฑ๋ก ์์คํ
์ ์ฌ์ฉํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ๋ ์ง์คํธ๋ฆฌ์ ๋ฑ๋ก
+ */
+export class MailRecipientSelectorRenderer extends AutoRegisteringComponentRenderer {
+ static componentDefinition = MailRecipientSelectorDefinition;
+
+ render(): React.ReactElement {
+ return ;
+ }
+}
+
+// ์๋ ๋ฑ๋ก ์คํ
+MailRecipientSelectorRenderer.registerSelf();
+
+// Hot Reload ์ง์ (๊ฐ๋ฐ ๋ชจ๋)
+if (process.env.NODE_ENV === "development") {
+ MailRecipientSelectorRenderer.enableHotReload();
+}
+
+export default MailRecipientSelectorRenderer;
diff --git a/frontend/lib/registry/components/mail-recipient-selector/index.ts b/frontend/lib/registry/components/mail-recipient-selector/index.ts
new file mode 100644
index 00000000..2631f854
--- /dev/null
+++ b/frontend/lib/registry/components/mail-recipient-selector/index.ts
@@ -0,0 +1,46 @@
+"use client";
+
+import { createComponentDefinition } from "../../utils/createComponentDefinition";
+import { ComponentCategory } from "@/types/component";
+import { MailRecipientSelectorComponent } from "./MailRecipientSelectorComponent";
+import { MailRecipientSelectorConfigPanel } from "./MailRecipientSelectorConfigPanel";
+import type { MailRecipientSelectorConfig } from "./types";
+
+/**
+ * MailRecipientSelector ์ปดํฌ๋ํธ ์ ์
+ * ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ (๋ด๋ถ ์ธ์ + ์ธ๋ถ ์ด๋ฉ์ผ)
+ */
+export const MailRecipientSelectorDefinition = createComponentDefinition({
+ id: "mail-recipient-selector",
+ name: "๋ฉ์ผ ์์ ์ ์ ํ",
+ nameEng: "Mail Recipient Selector",
+ description: "๋ฉ์ผ ๋ฐ์ก ์ ์์ ์/์ฐธ์กฐ๋ฅผ ์ ํํ๋ ์ปดํฌ๋ํธ (๋ด๋ถ ์ธ์ ์ ํ + ์ธ๋ถ ์ด๋ฉ์ผ ์
๋ ฅ)",
+ category: ComponentCategory.INPUT,
+ webType: "custom" as any,
+ component: MailRecipientSelectorComponent,
+ defaultConfig: {
+ toFieldName: "mailTo",
+ ccFieldName: "mailCc",
+ showCc: true,
+ showInternalSelector: true,
+ showExternalInput: true,
+ toLabel: "์์ ์",
+ ccLabel: "์ฐธ์กฐ(CC)",
+ required: true,
+ } as MailRecipientSelectorConfig,
+ defaultSize: { width: 400, height: 200 },
+ configPanel: MailRecipientSelectorConfigPanel,
+ icon: "Mail",
+ tags: ["๋ฉ์ผ", "์์ ์", "์ด๋ฉ์ผ", "์ ํ"],
+ version: "1.0.0",
+ author: "๊ฐ๋ฐํ",
+ documentation: "",
+});
+
+// ํ์
๋ด๋ณด๋ด๊ธฐ
+export type { MailRecipientSelectorConfig, Recipient, InternalUser } from "./types";
+
+// ์ปดํฌ๋ํธ ๋ด๋ณด๋ด๊ธฐ
+export { MailRecipientSelectorComponent } from "./MailRecipientSelectorComponent";
+export { MailRecipientSelectorRenderer } from "./MailRecipientSelectorRenderer";
+export { MailRecipientSelectorConfigPanel } from "./MailRecipientSelectorConfigPanel";
diff --git a/frontend/lib/registry/components/mail-recipient-selector/types.ts b/frontend/lib/registry/components/mail-recipient-selector/types.ts
new file mode 100644
index 00000000..abfc4c9f
--- /dev/null
+++ b/frontend/lib/registry/components/mail-recipient-selector/types.ts
@@ -0,0 +1,64 @@
+/**
+ * ๋ฉ์ผ ์์ ์ ์ ํ ์ปดํฌ๋ํธ ํ์
์ ์
+ */
+
+// ์์ ์ ์ ๋ณด
+export interface Recipient {
+ id: string;
+ email: string;
+ name?: string;
+ type: "internal" | "external"; // ๋ด๋ถ ์ฌ์ฉ์ ๋๋ ์ธ๋ถ ์ด๋ฉ์ผ
+ userId?: string; // ๋ด๋ถ ์ฌ์ฉ์์ธ ๊ฒฝ์ฐ ์ฌ์ฉ์ ID
+}
+
+// ์ปดํฌ๋ํธ ์ค์
+export interface MailRecipientSelectorConfig {
+ // ๊ธฐ๋ณธ ์ค์
+ toFieldName?: string; // formData์ ์ ์ฅํ ์์ ์ ํ๋๋ช
(๊ธฐ๋ณธ: mailTo)
+ ccFieldName?: string; // formData์ ์ ์ฅํ ์ฐธ์กฐ ํ๋๋ช
(๊ธฐ๋ณธ: mailCc)
+
+ // ํ์ ์ต์
+ showCc?: boolean; // ์ฐธ์กฐ(CC) ํ๋ ํ์ ์ฌ๋ถ (๊ธฐ๋ณธ: true)
+ showInternalSelector?: boolean; // ๋ด๋ถ ์ธ์ ์ ํ ํ์ ์ฌ๋ถ (๊ธฐ๋ณธ: true)
+ showExternalInput?: boolean; // ์ธ๋ถ ์ด๋ฉ์ผ ์
๋ ฅ ํ์ ์ฌ๋ถ (๊ธฐ๋ณธ: true)
+
+ // ๋ผ๋ฒจ
+ toLabel?: string; // ์์ ์ ๋ผ๋ฒจ (๊ธฐ๋ณธ: "์์ ์")
+ ccLabel?: string; // ์ฐธ์กฐ ๋ผ๋ฒจ (๊ธฐ๋ณธ: "์ฐธ์กฐ(CC)")
+
+ // ์ ํ
+ maxRecipients?: number; // ์ต๋ ์์ ์ ์ (๊ธฐ๋ณธ: ๋ฌด์ ํ)
+ maxCcRecipients?: number; // ์ต๋ ์ฐธ์กฐ ์์ ์ ์ (๊ธฐ๋ณธ: ๋ฌด์ ํ)
+
+ // ํ์ ์ฌ๋ถ
+ required?: boolean; // ์์ ์ ํ์ ์
๋ ฅ ์ฌ๋ถ (๊ธฐ๋ณธ: true)
+}
+
+// ์ปดํฌ๋ํธ Props
+export interface MailRecipientSelectorProps {
+ // ๊ธฐ๋ณธ Props
+ id?: string;
+ config?: MailRecipientSelectorConfig;
+
+ // ํผ ๋ฐ์ดํฐ ์ฐ๋
+ formData?: Record;
+ onFormDataChange?: (fieldName: string, value: any) => void;
+
+ // ์คํ์ผ
+ className?: string;
+ style?: React.CSSProperties;
+
+ // ๋ชจ๋
+ isPreviewMode?: boolean;
+ isInteractive?: boolean;
+}
+
+// ๋ด๋ถ ์ฌ์ฉ์ ์ ๋ณด (API ์๋ต - camelCase)
+export interface InternalUser {
+ userId: string;
+ userName: string;
+ email?: string;
+ deptName?: string;
+ positionName?: string;
+}
+
diff --git a/frontend/types/node-editor.ts b/frontend/types/node-editor.ts
index e47bae87..4c0b502b 100644
--- a/frontend/types/node-editor.ts
+++ b/frontend/types/node-editor.ts
@@ -407,6 +407,11 @@ export interface EmailActionNodeData {
// ๋ฉ์ผ ๊ณ์ ์ ํ (๋ฉ์ผ๊ด๋ฆฌ์์ ๋ฑ๋กํ ๊ณ์ )
accountId?: string; // ๋ฉ์ผ ๊ณ์ ID (์ฐ์ ์ฌ์ฉ)
+ // ๐ ์์ ์ ์ปดํฌ๋ํธ ์ฌ์ฉ ์ฌ๋ถ
+ useRecipientComponent?: boolean; // true๋ฉด {{mailTo}}, {{mailCc}} ์๋ ์ฌ์ฉ
+ recipientToField?: string; // ์์ ์ ํ๋๋ช
(๊ธฐ๋ณธ: mailTo)
+ recipientCcField?: string; // ์ฐธ์กฐ ํ๋๋ช
(๊ธฐ๋ณธ: mailCc)
+
// SMTP ์๋ฒ ์ค์ (์ง์ ์ค์ ์ ์ฌ์ฉ, accountId๊ฐ ์์ผ๋ฉด ๋ฌด์๋จ)
smtpConfig?: {
host: string;
@@ -420,8 +425,8 @@ export interface EmailActionNodeData {
// ๋ฉ์ผ ๋ด์ฉ
from?: string; // ๋ฐ์ ์ ์ด๋ฉ์ผ (๊ณ์ ์ ํ ์ ์๋ ์ค์ )
- to: string; // ์์ ์ ์ด๋ฉ์ผ (์ผํ๋ก ๊ตฌ๋ถํ์ฌ ์ฌ๋ฌ ๋ช
)
- cc?: string; // ์ฐธ์กฐ
+ to: string; // ์์ ์ ์ด๋ฉ์ผ (์ผํ๋ก ๊ตฌ๋ถํ์ฌ ์ฌ๋ฌ ๋ช
) - useRecipientComponent๊ฐ true๋ฉด ๋ฌด์๋จ
+ cc?: string; // ์ฐธ์กฐ - useRecipientComponent๊ฐ true๋ฉด ๋ฌด์๋จ
bcc?: string; // ์จ์ ์ฐธ์กฐ
subject: string; // ์ ๋ชฉ (ํ
ํ๋ฆฟ ๋ณ์ ์ง์)
body: string; // ๋ณธ๋ฌธ (ํ
ํ๋ฆฟ ๋ณ์ ์ง์)