From 1ee1287b8a8f5f167ae0b0bf22965efec389f149 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 9 Dec 2025 13:29:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../properties/EmailActionProperties.tsx | 142 +++++- frontend/lib/registry/components/index.ts | 3 + .../MailRecipientSelectorComponent.tsx | 458 ++++++++++++++++++ .../MailRecipientSelectorConfigPanel.tsx | 246 ++++++++++ .../MailRecipientSelectorRenderer.tsx | 33 ++ .../mail-recipient-selector/index.ts | 46 ++ .../mail-recipient-selector/types.ts | 64 +++ frontend/types/node-editor.ts | 9 +- 8 files changed, 979 insertions(+), 22 deletions(-) create mode 100644 frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorComponent.tsx create mode 100644 frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel.tsx create mode 100644 frontend/lib/registry/components/mail-recipient-selector/MailRecipientSelectorRenderer.tsx create mode 100644 frontend/lib/registry/components/mail-recipient-selector/index.ts create mode 100644 frontend/lib/registry/components/mail-recipient-selector/types.ts 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 && ( +
+
+
+ + { + const newField = e.target.value; + setRecipientToField(newField); + setTo(`{{${newField}}}`); + updateNodeData({ + recipientToField: newField, + to: `{{${newField}}}`, + }); + }} + placeholder="mailTo" + className="h-7 text-xs" + /> +
+
+ + { + const newField = e.target.value; + setRecipientCcField(newField); + setCc(`{{${newField}}}`); + updateNodeData({ + recipientCcField: newField, + cc: `{{${newField}}}`, + }); + }} + placeholder="mailCc" + className="h-7 text-xs" + /> +
+
+
+ ์ž๋™ ์„ค์ •๋จ: +
+ ์ˆ˜์‹ ์ž: {`{{${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" + /> +
+
+
+ + {/* ์‚ฌ์šฉ ์•ˆ๋‚ด */} + + +
+

์‚ฌ์šฉ ๋ฐฉ๋ฒ•

+
    +
  1. ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ชจ๋‹ฌ ํ™”๋ฉด์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
  2. +
  3. + ์ œ์–ด๊ด€๋ฆฌ์˜ ๋ฉ”์ผ ๋ฐœ์†ก ๋…ธ๋“œ์—์„œ ์ˆ˜์‹ ์ž๋ฅผ{" "} + {`{{mailTo}}`}๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. +
  4. +
  5. + ์ฐธ์กฐ๋Š”{" "} + {`{{mailCc}}`}๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. +
  6. +
  7. ๋ถ€๋ชจ ํ™”๋ฉด์—์„œ ์„ ํƒํ•œ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„ ๋ณ€์ˆ˜๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  8. +
+
+
+
+
+ ); +}; + +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; // ๋ณธ๋ฌธ (ํ…œํ”Œ๋ฆฟ ๋ณ€์ˆ˜ ์ง€์›)