From 62e11127a7724659f119d9cfc2783d4d5d2b1f38 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Sat, 7 Mar 2026 09:56:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(pop-button):=20=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?+=20=EC=97=B0=EA=B2=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20UX=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20+=20=ED=95=84=ED=84=B0=20UI=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20POP=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=B0=B1=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EC=96=B4=EA=B4=80=EB=A6=AC(node=5Fflows)?= =?UTF-8?q?=EB=A5=BC=20=EC=A7=81=EC=A0=91=20=EC=8B=A4=ED=96=89=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20"=EC=A0=9C=EC=96=B4?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89"=20=EC=9E=91=EC=97=85=20=EC=9C=A0?= =?UTF-8?q?=ED=98=95=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0,=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=84=A0=ED=83=9D=ED=95=98=EB=8A=94=20UX?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EC=84=A0=ED=95=9C=EB=8B=A4.=20[=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=20=EC=8B=A4=ED=96=89=20(custom-event=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5)]=20-=20ButtonTask=EC=97=90=20flowId/flowName=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20ControlFlowTaskFo?= =?UTF-8?q?rm:=20Combobox(Popover+Command)=EB=A1=9C=20=EA=B2=80=EC=83=89/?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20UI=20-=20executePopAction:=20flowId=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20POST=20/dataflow/node-flows/:flowId/execut?= =?UTF-8?q?e=20-=20=EA=B8=B0=EC=A1=B4=20eventName=20=EB=B0=9C=ED=96=89=20?= =?UTF-8?q?=EB=A9=94=EC=BB=A4=EB=8B=88=EC=A6=98=EC=9D=80=20=ED=8F=B4?= =?UTF-8?q?=EB=B0=B1=EC=9C=BC=EB=A1=9C=20=EC=9C=A0=EC=A7=80=20[=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20UX=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20-=20extractCardFields=20->=20extractConnectedField?= =?UTF-8?q?s=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=20=20(connections?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=EC=97=B0=EA=B2=B0=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=EB=A7=8C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EC=B6=9C)=20-=20pop-card-list/pop-field/p?= =?UTF-8?q?op-search=20=ED=83=80=EC=9E=85=EB=B3=84=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20-=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=ED=95=84=EB=93=9C(=5F=5Fcart=5Fquantity?= =?UTF-8?q?=20=EB=93=B1)=EC=97=90=20=ED=95=9C=EA=B8=80=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8=20=EB=B6=80=EC=97=AC=20-=20UI=20=EB=9D=BC=EB=B2=A8:?= =?UTF-8?q?=20"=ED=99=94=EB=A9=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0"=20->=20"?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0"=20[p?= =?UTF-8?q?op-card-list=20=ED=95=84=ED=84=B0=20UI]=20-=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=EA=B1=B4=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=9D=84=20=EA=B0=80=EB=A1=9C=20->=20=EC=84=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=8A=A4=ED=83=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20=EC=A1=B0=EA=B1=B4=20=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20+=20=EC=9E=85=EB=A0=A5=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=86=92=EC=9D=B4=20=ED=99=95=EB=8C=80=20[?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95]=20-=20apiClient=20base?= =?UTF-8?q?URL=20=EC=9D=B4=EC=A4=91=20/api=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20=EC=9D=91=EB=8B=B5=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20camelCase=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/pop/executePopAction.ts | 4 +- .../registry/pop-components/pop-button.tsx | 246 ++++++++++++++---- .../pop-card-list/PopCardListConfig.tsx | 169 ++++++------ 3 files changed, 294 insertions(+), 125 deletions(-) diff --git a/frontend/hooks/pop/executePopAction.ts b/frontend/hooks/pop/executePopAction.ts index ada6ad77..ad0981b6 100644 --- a/frontend/hooks/pop/executePopAction.ts +++ b/frontend/hooks/pop/executePopAction.ts @@ -322,7 +322,9 @@ export async function executeTaskList( } case "custom-event": - if (task.eventName) { + if (task.flowId) { + await apiClient.post(`/dataflow/node-flows/${task.flowId}/execute`, {}); + } else if (task.eventName) { publish(task.eventName, task.eventPayload ?? {}); } break; diff --git a/frontend/lib/registry/pop-components/pop-button.tsx b/frontend/lib/registry/pop-components/pop-button.tsx index 56889eff..67aaabad 100644 --- a/frontend/lib/registry/pop-components/pop-button.tsx +++ b/frontend/lib/registry/pop-components/pop-button.tsx @@ -23,6 +23,19 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry"; import { usePopAction } from "@/hooks/pop/usePopAction"; import { executeTaskList, type CollectedPayload } from "@/hooks/pop/executePopAction"; @@ -51,6 +64,7 @@ import { PackageCheck, ChevronRight, GripVertical, + ChevronsUpDown, type LucideIcon, } from "lucide-react"; import { toast } from "sonner"; @@ -227,9 +241,11 @@ export interface ButtonTask { apiEndpoint?: string; apiMethod?: "GET" | "POST" | "PUT" | "DELETE"; - // custom-event + // custom-event (제어 실행) eventName?: string; eventPayload?: Record; + flowId?: number; + flowName?: string; } /** 빠른 시작 템플릿 */ @@ -345,7 +361,7 @@ export const TASK_TYPE_LABELS: Record = { "close-modal": "모달 닫기", "refresh": "새로고침", "api-call": "API 호출", - "custom-event": "커스텀 이벤트", + "custom-event": "제어 실행", }; /** 빠른 시작 템플릿별 기본 작업 목록 + 외형 */ @@ -1267,39 +1283,80 @@ interface PopButtonConfigPanelProps { componentId?: string; } -/** 화면 내 카드 컴포넌트에서 사용 가능한 필드 목록 추출 */ -function extractCardFields( +/** 연결된 컴포넌트에서 사용 가능한 필드 목록 추출 (연결 기반) */ +function extractConnectedFields( + componentId?: string, + connections?: PopButtonConfigPanelProps["connections"], allComponents?: PopButtonConfigPanelProps["allComponents"], ): { value: string; label: string; source: string }[] { - if (!allComponents) return []; + if (!componentId || !connections || !allComponents) return []; + + const targetIds = connections + .filter((c) => c.sourceComponent === componentId || c.targetComponent === componentId) + .map((c) => (c.sourceComponent === componentId ? c.targetComponent : c.sourceComponent)); + const uniqueIds = [...new Set(targetIds)]; + if (uniqueIds.length === 0) return []; + const fields: { value: string; label: string; source: string }[] = []; - for (const comp of allComponents) { - if (comp.type !== "pop-card-list" || !comp.config) continue; - const tpl = (comp.config as Record).cardTemplate as - | { header?: Record; body?: { fields?: { id?: string; label?: string; valueType?: string; columnName?: string }[] } } - | undefined; - if (!tpl) continue; + for (const tid of uniqueIds) { + const comp = allComponents.find((c) => c.id === tid); + if (!comp?.config) continue; + const cfg = comp.config as Record; + const compLabel = (comp as Record).label as string || comp.type || tid; - if (tpl.header?.codeField) { - fields.push({ value: String(tpl.header.codeField), label: String(tpl.header.codeField), source: "헤더 코드" }); + if (comp.type === "pop-card-list") { + const tpl = cfg.cardTemplate as + | { header?: Record; body?: { fields?: { id?: string; label?: string; valueType?: string; columnName?: string }[] } } + | undefined; + if (tpl) { + if (tpl.header?.codeField) { + fields.push({ value: String(tpl.header.codeField), label: String(tpl.header.codeField), source: compLabel }); + } + if (tpl.header?.titleField) { + fields.push({ value: String(tpl.header.titleField), label: String(tpl.header.titleField), source: compLabel }); + } + for (const f of tpl.body?.fields ?? []) { + if (f.valueType === "column" && f.columnName) { + fields.push({ value: f.columnName, label: f.label || f.columnName, source: compLabel }); + } else if (f.valueType === "formula" && f.label) { + fields.push({ value: `__formula_${f.id || f.label}`, label: f.label, source: `${compLabel} (수식)` }); + } + } + } + fields.push({ value: "__cart_quantity", label: "사용자 입력 수량", source: `${compLabel} (장바구니)` }); + fields.push({ value: "__cart_row_key", label: "선택한 카드의 원본 키", source: `${compLabel} (장바구니)` }); + fields.push({ value: "__cart_id", label: "장바구니 항목 ID", source: `${compLabel} (장바구니)` }); } - if (tpl.header?.titleField) { - fields.push({ value: String(tpl.header.titleField), label: String(tpl.header.titleField), source: "헤더 제목" }); - } - for (const f of tpl.body?.fields ?? []) { - if (f.valueType === "column" && f.columnName) { - fields.push({ value: f.columnName, label: f.label || f.columnName, source: "본문" }); - } else if (f.valueType === "formula" && f.label) { - const formulaKey = `__formula_${f.id || f.label}`; - fields.push({ value: formulaKey, label: f.label, source: "수식" }); + + if (comp.type === "pop-field") { + const sections = cfg.sections as Array<{ + fields?: Array<{ id: string; fieldName?: string; labelText?: string }>; + }> | undefined; + if (Array.isArray(sections)) { + for (const section of sections) { + for (const f of section.fields ?? []) { + if (f.fieldName) { + fields.push({ value: f.fieldName, label: f.labelText || f.fieldName, source: compLabel }); + } + } + } } } - // 시스템 필드 추가 - fields.push({ value: "__cart_quantity", label: "수량 (장바구니)", source: "시스템" }); - fields.push({ value: "__cart_row_key", label: "원본 키", source: "시스템" }); - fields.push({ value: "__cart_id", label: "카드 항목 ID", source: "시스템" }); + if (comp.type === "pop-search") { + const filterCols = cfg.filterColumns as string[] | undefined; + const modalCfg = cfg.modalConfig as { valueField?: string } | undefined; + if (Array.isArray(filterCols) && filterCols.length > 0) { + for (const col of filterCols) { + fields.push({ value: col, label: col, source: compLabel }); + } + } else if (modalCfg?.valueField) { + fields.push({ value: modalCfg.valueField, label: modalCfg.valueField, source: compLabel }); + } else if (cfg.fieldName && typeof cfg.fieldName === "string") { + fields.push({ value: cfg.fieldName, label: (cfg.placeholder as string) || cfg.fieldName, source: compLabel }); + } + } } return fields; @@ -1309,9 +1366,14 @@ export function PopButtonConfigPanel({ config, onUpdate, allComponents, + connections, + componentId, }: PopButtonConfigPanelProps) { const v2 = useMemo(() => migrateButtonConfig(config), [config]); - const cardFields = useMemo(() => extractCardFields(allComponents), [allComponents]); + const cardFields = useMemo( + () => extractConnectedFields(componentId, connections, allComponents), + [componentId, connections, allComponents], + ); const updateV2 = useCallback( (partial: Partial) => { @@ -1560,7 +1622,7 @@ function buildTaskSummary(task: ButtonTask): string { case "api-call": return task.apiEndpoint || ""; case "custom-event": - return task.eventName || ""; + return task.flowName || task.eventName || ""; default: return ""; } @@ -1829,15 +1891,10 @@ function TaskDetailForm({ case "custom-event": return ( -
- - onUpdate({ eventName: e.target.value })} - placeholder="예: data-saved, item-selected" - className="h-8 text-xs" - /> -
+ ); case "refresh": @@ -1897,7 +1954,7 @@ function buildUpdateSummaryText(task: ButtonTask): string | null { } const val = task.valueSource === "linked" - ? `화면 데이터(${task.sourceField || "?"})` + ? `연결 데이터(${task.sourceField || "?"})` : `"${task.fixedValue || "?"}"`; return `[${task.targetTable}].${task.targetColumn} ${opLabel} ${val}`; } @@ -2003,7 +2060,7 @@ function DataUpdateTaskForm({ 직접 입력 - 화면 데이터에서 가져오기 + 연결된 데이터 가져오기 @@ -2022,19 +2079,19 @@ function DataUpdateTaskForm({ {task.valueSource === "linked" && (
- + {cardFields.length > 0 ? ( updateFilter(index, { ...filter, column: val }) } > - - + + {columns.map((col) => ( @@ -2058,45 +2071,36 @@ function FilterSettingsSection({ ))} - - - - - updateFilter(index, { ...filter, value: e.target.value }) - } - placeholder="값" - className="h-7 flex-1 text-xs" - /> - - +
+ + + updateFilter(index, { ...filter, value: e.target.value }) + } + placeholder="값 입력" + className="h-8 flex-1 text-xs" + /> +
))} @@ -2663,46 +2667,51 @@ function FilterCriteriaSection({ ) : (
{filters.map((filter, index) => ( -
-
- updateFilter(index, { ...filter, column: val || "" })} - placeholder="컬럼 선택" +
+
+ + 조건 {index + 1} + + +
+ updateFilter(index, { ...filter, column: val || "" })} + placeholder="컬럼 선택" + /> +
+ + updateFilter(index, { ...filter, value: e.target.value })} + placeholder="값 입력" + className="h-8 flex-1 text-xs" />
- - updateFilter(index, { ...filter, value: e.target.value })} - placeholder="값" - className="h-7 flex-1 text-xs" - /> -
))}