"use client"; import React, { useCallback, useState, useEffect, useMemo } from "react"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry"; import { DataFlowAPI } from "@/lib/api/dataflow"; import { usePopAction } from "@/hooks/pop/usePopAction"; import { usePopDesignerContext } from "@/components/pop/designer/PopDesignerContext"; import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { Save, Trash2, LogOut, Menu, ExternalLink, Plus, Check, X, Edit, Search, RefreshCw, Download, Upload, Send, Copy, Settings, ChevronDown, ShoppingCart, ShoppingBag, type LucideIcon, } from "lucide-react"; import { toast } from "sonner"; // ======================================== // STEP 1: 타입 정의 // ======================================== /** 메인 액션 타입 (5종) */ export type ButtonActionType = "save" | "delete" | "api" | "modal" | "event"; /** 후속 액션 타입 (4종) */ export type FollowUpActionType = "event" | "refresh" | "navigate" | "close-modal"; /** 버튼 variant (shadcn 기반 4종) */ export type ButtonVariant = "default" | "secondary" | "outline" | "destructive"; /** 모달 열기 방식 */ export type ModalMode = "dropdown" | "fullscreen" | "screen-ref"; /** 확인 다이얼로그 설정 */ export interface ConfirmConfig { enabled: boolean; message?: string; // 빈값이면 기본 메시지 } /** 후속 액션 1건 */ export interface FollowUpAction { type: FollowUpActionType; // event eventName?: string; eventPayload?: Record; // navigate targetScreenId?: string; params?: Record; } /** 드롭다운 모달 메뉴 항목 */ export interface ModalMenuItem { label: string; screenId?: string; action?: string; // 커스텀 이벤트명 } /** 메인 액션 설정 */ export interface ButtonMainAction { type: ButtonActionType; // save/delete 공통 targetTable?: string; // api apiEndpoint?: string; apiMethod?: "GET" | "POST" | "PUT" | "DELETE"; // modal modalMode?: ModalMode; modalScreenId?: string; modalTitle?: string; modalItems?: ModalMenuItem[]; // event eventName?: string; eventPayload?: Record; } /** 프리셋 이름 */ export type ButtonPreset = | "save" | "delete" | "logout" | "menu" | "modal-open" | "cart" | "custom"; /** row_data 저장 모드 */ export type RowDataMode = "all" | "selected"; /** 장바구니 버튼 전용 설정 */ export interface CartButtonConfig { cartScreenId?: string; rowDataMode?: RowDataMode; selectedColumns?: string[]; } /** pop-button 전체 설정 */ export interface PopButtonConfig { label: string; variant: ButtonVariant; icon?: string; iconOnly?: boolean; preset: ButtonPreset; confirm?: ConfirmConfig; action: ButtonMainAction; followUpActions?: FollowUpAction[]; cart?: CartButtonConfig; } // ======================================== // 상수 // ======================================== /** 메인 액션 타입 라벨 */ const ACTION_TYPE_LABELS: Record = { save: "저장", delete: "삭제", api: "API 호출", modal: "모달 열기", event: "이벤트 발행", }; /** 후속 액션 타입 라벨 */ const FOLLOWUP_TYPE_LABELS: Record = { event: "이벤트 발행", refresh: "새로고침", navigate: "화면 이동", "close-modal": "모달 닫기", }; /** variant 라벨 */ const VARIANT_LABELS: Record = { default: "기본 (Primary)", secondary: "보조 (Secondary)", outline: "외곽선 (Outline)", destructive: "위험 (Destructive)", }; /** 프리셋 라벨 */ const PRESET_LABELS: Record = { save: "저장", delete: "삭제", logout: "로그아웃", menu: "메뉴 (드롭다운)", "modal-open": "모달 열기", cart: "장바구니 저장", custom: "직접 설정", }; /** 모달 모드 라벨 */ const MODAL_MODE_LABELS: Record = { dropdown: "드롭다운", fullscreen: "전체 모달", "screen-ref": "화면 선택", }; /** API 메서드 라벨 */ const API_METHOD_LABELS: Record = { GET: "GET", POST: "POST", PUT: "PUT", DELETE: "DELETE", }; /** 주요 Lucide 아이콘 목록 (설정 패널용) */ const ICON_OPTIONS: { value: string; label: string }[] = [ { value: "none", label: "없음" }, { value: "Save", label: "저장 (Save)" }, { value: "Trash2", label: "삭제 (Trash)" }, { value: "LogOut", label: "로그아웃 (LogOut)" }, { value: "Menu", label: "메뉴 (Menu)" }, { value: "ExternalLink", label: "외부링크 (ExternalLink)" }, { value: "Plus", label: "추가 (Plus)" }, { value: "Check", label: "확인 (Check)" }, { value: "X", label: "취소 (X)" }, { value: "Edit", label: "수정 (Edit)" }, { value: "Search", label: "검색 (Search)" }, { value: "RefreshCw", label: "새로고침 (RefreshCw)" }, { value: "Download", label: "다운로드 (Download)" }, { value: "Upload", label: "업로드 (Upload)" }, { value: "Send", label: "전송 (Send)" }, { value: "Copy", label: "복사 (Copy)" }, { value: "Settings", label: "설정 (Settings)" }, { value: "ChevronDown", label: "아래 화살표 (ChevronDown)" }, { value: "ShoppingCart", label: "장바구니 (ShoppingCart)" }, { value: "ShoppingBag", label: "장바구니 담김 (ShoppingBag)" }, ]; /** 프리셋별 기본 설정 */ const PRESET_DEFAULTS: Record> = { save: { label: "저장", variant: "default", icon: "Save", confirm: { enabled: false }, action: { type: "save" }, }, delete: { label: "삭제", variant: "destructive", icon: "Trash2", confirm: { enabled: true, message: "" }, action: { type: "delete" }, }, logout: { label: "로그아웃", variant: "outline", icon: "LogOut", confirm: { enabled: true, message: "로그아웃 하시겠습니까?" }, action: { type: "api", apiEndpoint: "/api/auth/logout", apiMethod: "POST", }, }, menu: { label: "메뉴", variant: "secondary", icon: "Menu", confirm: { enabled: false }, action: { type: "modal", modalMode: "dropdown" }, }, "modal-open": { label: "열기", variant: "outline", icon: "ExternalLink", confirm: { enabled: false }, action: { type: "modal", modalMode: "fullscreen" }, }, cart: { label: "장바구니 저장", variant: "default", icon: "ShoppingCart", confirm: { enabled: true, message: "장바구니에 담은 항목을 저장하시겠습니까?" }, action: { type: "event" }, }, custom: { label: "버튼", variant: "default", icon: "none", confirm: { enabled: false }, action: { type: "save" }, }, }; /** 확인 다이얼로그 기본 메시지 (액션별) */ const DEFAULT_CONFIRM_MESSAGES: Record = { save: "저장하시겠습니까?", delete: "삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", api: "실행하시겠습니까?", modal: "열기하시겠습니까?", event: "실행하시겠습니까?", }; // ======================================== // 헬퍼 함수 // ======================================== /** 섹션 구분선 */ function SectionDivider({ label }: { label: string }) { return (
{label}
); } /** 장바구니 데이터 매핑 행 (읽기 전용) */ function CartMappingRow({ source, target, desc, auto, }: { source: string; target: string; desc?: string; auto?: boolean; }) { return (
{source}
{target} {desc && (

{desc}

)}
); } /** 허용된 아이콘 맵 (개별 import로 트리 쉐이킹 적용) */ const LUCIDE_ICON_MAP: Record = { Save, Trash2, LogOut, Menu, ExternalLink, Plus, Check, X, Edit, Search, RefreshCw, Download, Upload, Send, Copy, Settings, ChevronDown, ShoppingCart, ShoppingBag, }; /** Lucide 아이콘 동적 렌더링 */ function DynamicLucideIcon({ name, size = 16, className, }: { name: string; size?: number; className?: string; }) { const IconComponent = LUCIDE_ICON_MAP[name]; if (!IconComponent) return null; return ; } // ======================================== // STEP 2: 메인 컴포넌트 // ======================================== interface PopButtonComponentProps { config?: PopButtonConfig; label?: string; isDesignMode?: boolean; screenId?: string; componentId?: string; } export function PopButtonComponent({ config, label, isDesignMode, screenId, componentId, }: PopButtonComponentProps) { const { execute, isLoading, pendingConfirm, confirmExecute, cancelConfirm, } = usePopAction(screenId || ""); const { subscribe, publish } = usePopEvent(screenId || "default"); // 장바구니 모드 상태 const isCartMode = config?.preset === "cart"; const [cartCount, setCartCount] = useState(0); const [cartIsDirty, setCartIsDirty] = useState(false); const [cartSaving, setCartSaving] = useState(false); const [showCartConfirm, setShowCartConfirm] = useState(false); // 장바구니 상태 수신 (카드 목록에서 count/isDirty 전달) useEffect(() => { if (!isCartMode || !componentId) return; const unsub = subscribe( `__comp_input__${componentId}__cart_updated`, (payload: unknown) => { const data = payload as { value?: { count?: number; isDirty?: boolean } } | undefined; const inner = data?.value; if (inner?.count !== undefined) setCartCount(inner.count); if (inner?.isDirty !== undefined) setCartIsDirty(inner.isDirty); } ); return unsub; }, [isCartMode, componentId, subscribe]); // 저장 완료 수신 (카드 목록에서 saveToDb 완료 후 전달) const cartScreenIdRef = React.useRef(config?.cart?.cartScreenId); cartScreenIdRef.current = config?.cart?.cartScreenId; useEffect(() => { if (!isCartMode || !componentId) return; const unsub = subscribe( `__comp_input__${componentId}__cart_save_completed`, (payload: unknown) => { const data = payload as { value?: { success?: boolean } } | undefined; setCartSaving(false); if (data?.value?.success) { setCartIsDirty(false); const targetScreenId = cartScreenIdRef.current; if (targetScreenId) { const cleanId = targetScreenId.replace(/^.*\/(\d+)$/, "$1").trim(); window.location.href = `/pop/screens/${cleanId}`; } else { toast.success("장바구니가 저장되었습니다."); } } else { toast.error("장바구니 저장에 실패했습니다."); } } ); return unsub; }, [isCartMode, componentId, subscribe]); const getConfirmMessage = useCallback((): string => { if (pendingConfirm?.confirm?.message) return pendingConfirm.confirm.message; if (config?.confirm?.message) return config.confirm.message; return DEFAULT_CONFIRM_MESSAGES[config?.action?.type || "save"]; }, [pendingConfirm?.confirm?.message, config?.confirm?.message, config?.action?.type]); // 장바구니 저장 트리거 (연결 미설정 시 10초 타임아웃으로 복구) const cartSaveTimeoutRef = React.useRef | null>(null); const handleCartSave = useCallback(() => { if (!componentId) return; setCartSaving(true); const selectedCols = config?.cart?.rowDataMode === "selected" ? config.cart.selectedColumns : undefined; publish(`__comp_output__${componentId}__cart_save_trigger`, { selectedColumns: selectedCols, }); if (cartSaveTimeoutRef.current) clearTimeout(cartSaveTimeoutRef.current); cartSaveTimeoutRef.current = setTimeout(() => { setCartSaving((prev) => { if (prev) { toast.error("장바구니 저장 응답이 없습니다. 연결 설정을 확인하세요."); } return false; }); }, 10_000); }, [componentId, publish, config?.cart?.rowDataMode, config?.cart?.selectedColumns]); // 저장 완료 시 타임아웃 정리 useEffect(() => { if (!cartSaving && cartSaveTimeoutRef.current) { clearTimeout(cartSaveTimeoutRef.current); cartSaveTimeoutRef.current = null; } }, [cartSaving]); // 클릭 핸들러 const handleClick = useCallback(async () => { if (isDesignMode) { toast.info( `[디자인 모드] ${isCartMode ? "장바구니 저장" : ACTION_TYPE_LABELS[config?.action?.type || "save"]} 액션` ); return; } // 장바구니 모드: isDirty 여부에 따라 분기 if (isCartMode) { if (cartCount === 0 && !cartIsDirty) { toast.info("장바구니가 비어 있습니다."); return; } if (cartIsDirty) { // 새로 담은 항목이 있음 → 확인 후 저장 setShowCartConfirm(true); } else { // 이미 저장된 상태 → 바로 장바구니 화면 이동 const targetScreenId = config?.cart?.cartScreenId; if (targetScreenId) { const cleanId = targetScreenId.replace(/^.*\/(\d+)$/, "$1").trim(); window.location.href = `/pop/screens/${cleanId}`; } else { toast.info("장바구니 화면이 설정되지 않았습니다."); } } return; } const action = config?.action; if (!action) return; await execute(action, { confirm: config?.confirm, followUpActions: config?.followUpActions, }); }, [isDesignMode, isCartMode, config, cartCount, cartIsDirty, execute, handleCartSave]); // 외형 const buttonLabel = config?.label || label || "버튼"; const variant = config?.variant || "default"; const iconName = config?.icon || ""; const isIconOnly = config?.iconOnly || false; // 장바구니 3상태 아이콘: 빈 장바구니 / 저장 완료 / 변경사항 있음 const cartIconName = useMemo(() => { if (!isCartMode) return iconName; if (cartCount === 0 && !cartIsDirty) return "ShoppingCart"; if (cartCount > 0 && !cartIsDirty) return "ShoppingBag"; return "ShoppingCart"; }, [isCartMode, cartCount, cartIsDirty, iconName]); // 장바구니 3상태 버튼 색상 const cartButtonClass = useMemo(() => { if (!isCartMode) return ""; if (cartCount > 0 && !cartIsDirty) { return "bg-emerald-600 hover:bg-emerald-700 text-white border-emerald-600"; } if (cartIsDirty) { return "bg-orange-500 hover:bg-orange-600 text-white border-orange-500 animate-pulse"; } return ""; }, [isCartMode, cartCount, cartIsDirty]); return ( <>
{/* 장바구니 배지 */} {isCartMode && cartCount > 0 && (
{cartCount}
)}
{/* 장바구니 확인 다이얼로그 */} 장바구니 저장 {config?.confirm?.message || `${cartCount}개 항목을 장바구니에 저장하시겠습니까?`} 취소 { setShowCartConfirm(false); handleCartSave(); }} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm" > 저장 {/* 일반 확인 다이얼로그 */} { if (!open) cancelConfirm(); }}> 실행 확인 {getConfirmMessage()} 취소 확인 ); } // ======================================== // STEP 3: 설정 패널 // ======================================== interface PopButtonConfigPanelProps { config: PopButtonConfig; onUpdate: (config: PopButtonConfig) => void; allComponents?: { id: string; type: string; config?: Record }[]; connections?: { sourceComponent: string; targetComponent: string; sourceOutput?: string; targetInput?: string }[]; componentId?: string; } export function PopButtonConfigPanel({ config, onUpdate, allComponents, connections, componentId, }: PopButtonConfigPanelProps) { const isCustom = config?.preset === "custom"; // 컬럼 불러오기용 상태 const [loadedColumns, setLoadedColumns] = useState<{ name: string; label: string }[]>([]); const [colLoading, setColLoading] = useState(false); const [connectedTableName, setConnectedTableName] = useState(null); // 연결된 카드 목록의 테이블명 자동 탐색 useEffect(() => { if (config?.preset !== "cart" || !componentId || !connections || !allComponents) { setConnectedTableName(null); return; } // 방법 1: 버튼(source) -> 카드목록(target) cart_save_trigger 연결 let cardListId: string | undefined; const outConn = connections.find( (c) => c.sourceComponent === componentId && c.sourceOutput === "cart_save_trigger", ); if (outConn) { cardListId = outConn.targetComponent; } // 방법 2: 카드목록(source) -> 버튼(target) cart_updated 연결 (역방향) if (!cardListId) { const inConn = connections.find( (c) => c.targetComponent === componentId && (c.sourceOutput === "cart_updated" || c.sourceOutput === "cart_save_completed"), ); if (inConn) { cardListId = inConn.sourceComponent; } } // 방법 3: 버튼과 연결된 pop-card-list 타입 컴포넌트 탐색 if (!cardListId) { const anyConn = connections.find( (c) => (c.sourceComponent === componentId || c.targetComponent === componentId), ); if (anyConn) { const otherId = anyConn.sourceComponent === componentId ? anyConn.targetComponent : anyConn.sourceComponent; const otherComp = allComponents.find((c) => c.id === otherId); if (otherComp?.type === "pop-card-list") { cardListId = otherId; } } } if (!cardListId) { setConnectedTableName(null); return; } const cardList = allComponents.find((c) => c.id === cardListId); const cfg = cardList?.config as Record | undefined; const dataSource = cfg?.dataSource as Record | undefined; const tableName = (dataSource?.tableName as string) || (cfg?.tableName as string) || undefined; setConnectedTableName(tableName || null); }, [config?.preset, componentId, connections, allComponents]); // 선택 저장 모드 + 연결 테이블명이 있으면 컬럼 자동 로드 useEffect(() => { if (config?.cart?.rowDataMode !== "selected" || !connectedTableName) { return; } // 이미 같은 테이블의 컬럼이 로드되어 있으면 스킵 if (loadedColumns.length > 0) return; let cancelled = false; setColLoading(true); DataFlowAPI.getTableColumns(connectedTableName) .then((cols) => { if (cancelled) return; setLoadedColumns( cols .filter((c: { columnName: string }) => !["id", "created_at", "updated_at", "created_by", "updated_by"].includes(c.columnName), ) .map((c: { columnName: string; displayName?: string }) => ({ name: c.columnName, label: c.displayName || c.columnName, })), ); }) .catch(() => { if (!cancelled) setLoadedColumns([]); }) .finally(() => { if (!cancelled) setColLoading(false); }); return () => { cancelled = true; }; }, [config?.cart?.rowDataMode, connectedTableName, loadedColumns.length]); // 프리셋 변경 핸들러 const handlePresetChange = (preset: ButtonPreset) => { const defaults = PRESET_DEFAULTS[preset]; onUpdate({ ...config, preset, label: defaults.label || config.label, variant: defaults.variant || config.variant, icon: defaults.icon ?? config.icon, confirm: defaults.confirm || config.confirm, action: (defaults.action as ButtonMainAction) || config.action, // 후속 액션은 프리셋 변경 시 유지 }); }; // 메인 액션 업데이트 헬퍼 const updateAction = (updates: Partial) => { onUpdate({ ...config, action: { ...config.action, ...updates }, }); }; return (
{/* 프리셋 선택 */} {!isCustom && (

프리셋 변경 시 외형과 액션이 자동 설정됩니다

)} {/* 외형 설정 */}
{/* 라벨 */}
onUpdate({ ...config, label: e.target.value })} placeholder="버튼 텍스트" className="h-8 text-xs" />
{/* variant */}
{/* 아이콘 */}
{/* 아이콘 전용 모드 */}
onUpdate({ ...config, iconOnly: checked === true }) } />
{/* 장바구니 설정 (cart 프리셋 전용) */} {config?.preset === "cart" && ( <>
onUpdate({ ...config, cart: { ...config.cart, cartScreenId: e.target.value }, }) } placeholder="저장 후 이동할 POP 화면 ID" className="h-8 text-xs" />

저장 완료 후 이동할 장바구니 리스트 화면 ID입니다. 비어있으면 이동 없이 저장만 합니다.

{/* 데이터 저장 흐름 시각화 */}

카드 목록에서 "담기" 클릭 시 아래와 같이 cart_items 테이블에 저장됩니다.

{/* 사용자 입력 데이터 */}

사용자 입력

{/* 원본 데이터 */}

원본 행 데이터

{/* 저장 모드 선택 */}
저장 모드:
{config?.cart?.rowDataMode === "selected" ? ( <> {/* 선택 저장 모드: 컬럼 목록 관리 */}
{connectedTableName ? (

연결: {connectedTableName}

) : (

카드 목록과 연결(cart_save_trigger)하면 컬럼이 자동으로 표시됩니다.

)} {colLoading && (

컬럼 불러오는 중...

)} {/* 불러온 컬럼 체크박스 */} {loadedColumns.length > 0 && (
{loadedColumns.map((col) => { const isChecked = (config?.cart?.selectedColumns || []).includes(col.name); return ( ); })}
)} {/* 선택된 컬럼 요약 */} {(config?.cart?.selectedColumns?.length ?? 0) > 0 ? ( ) : (

저장할 컬럼을 선택하세요. 미선택 시 전체 저장됩니다.

)}
) : ( )}
{/* 시스템 자동 */}

자동 설정

장바구니 목록 화면에서 row_data의 JSON을 풀어서 최종 대상 테이블로 매핑할 수 있습니다.

)} {/* 메인 액션 (cart 프리셋에서는 숨김) */} {config?.preset !== "cart" && ( <>
{!isCustom && (

프리셋에 의해 고정됨. 변경하려면 "직접 설정" 선택

)}
)} {/* 확인 다이얼로그 */}
onUpdate({ ...config, confirm: { ...config?.confirm, enabled: checked === true, }, }) } />
{config?.confirm?.enabled && (
onUpdate({ ...config, confirm: { ...config?.confirm, enabled: true, message: e.target.value, }, }) } placeholder="비워두면 기본 메시지 사용" className="h-8 text-xs" />

기본:{" "} {DEFAULT_CONFIRM_MESSAGES[config?.action?.type || "save"]}

)}
{/* 후속 액션 */} onUpdate({ ...config, followUpActions: actions }) } />
); } // ======================================== // 액션 세부 필드 (타입별) // ======================================== function ActionDetailFields({ action, onUpdate, disabled, }: { action?: ButtonMainAction; onUpdate: (updates: Partial) => void; disabled?: boolean; }) { // 디자이너 컨텍스트 (뷰어에서는 null) const designerCtx = usePopDesignerContext(); const actionType = action?.type || "save"; switch (actionType) { case "save": case "delete": return (
onUpdate({ targetTable: e.target.value })} placeholder="테이블명 입력" className="h-8 text-xs" disabled={disabled} />
); case "api": return (
onUpdate({ apiEndpoint: e.target.value })} placeholder="/api/..." className="h-8 text-xs" disabled={disabled} />
); case "modal": return (
{action?.modalMode === "screen-ref" && (
onUpdate({ modalScreenId: e.target.value }) } placeholder="화면 ID" className="h-8 text-xs" disabled={disabled} />
)}
onUpdate({ modalTitle: e.target.value })} placeholder="모달 제목 (선택)" className="h-8 text-xs" disabled={disabled} />
{/* 모달 캔버스 생성/열기 (fullscreen 모드 + 디자이너 내부) */} {action?.modalMode === "fullscreen" && designerCtx && (
{action?.modalScreenId ? ( ) : ( )}
)}
); case "event": return (
onUpdate({ eventName: e.target.value })} placeholder="예: data-saved, item-selected" className="h-8 text-xs" disabled={disabled} />
); default: return null; } } // ======================================== // 후속 액션 편집기 // ======================================== function FollowUpActionsEditor({ actions, onUpdate, }: { actions: FollowUpAction[]; onUpdate: (actions: FollowUpAction[]) => void; }) { // 추가 const handleAdd = () => { onUpdate([...actions, { type: "event" }]); }; // 삭제 const handleRemove = (index: number) => { onUpdate(actions.filter((_, i) => i !== index)); }; // 수정 const handleUpdate = (index: number, updates: Partial) => { const newActions = [...actions]; newActions[index] = { ...newActions[index], ...updates }; onUpdate(newActions); }; return (
{actions.length === 0 && (

메인 액션 성공 후 순차 실행할 후속 동작

)} {actions.map((fa, idx) => (
후속 {idx + 1}
{/* 타입 선택 */} {/* 타입별 추가 입력 */} {fa.type === "event" && ( handleUpdate(idx, { eventName: e.target.value }) } placeholder="이벤트명" className="h-7 text-xs" /> )} {fa.type === "navigate" && ( handleUpdate(idx, { targetScreenId: e.target.value }) } placeholder="화면 ID" className="h-7 text-xs" /> )}
))}
); } // ======================================== // STEP 4: 미리보기 + 레지스트리 등록 // ======================================== function PopButtonPreviewComponent({ config, }: { config?: PopButtonConfig; }) { const buttonLabel = config?.label || "버튼"; const variant = config?.variant || "default"; const iconName = config?.icon || ""; const isIconOnly = config?.iconOnly || false; return (
); } // 레지스트리 등록 PopComponentRegistry.registerComponent({ id: "pop-button", name: "버튼", description: "액션 버튼 (저장/삭제/API/모달/이벤트/장바구니)", category: "action", icon: "MousePointerClick", component: PopButtonComponent, configPanel: PopButtonConfigPanel, preview: PopButtonPreviewComponent, defaultProps: { label: "버튼", variant: "default", preset: "custom", confirm: { enabled: false }, action: { type: "save" }, } as PopButtonConfig, connectionMeta: { sendable: [ { key: "cart_save_trigger", label: "저장 요청", type: "event", description: "장바구니 일괄 저장 요청 (카드 목록에 전달)" }, ], receivable: [ { key: "cart_updated", label: "장바구니 상태", type: "event", description: "카드 목록에서 장바구니 변경 시 count/isDirty 수신" }, { key: "cart_save_completed", label: "저장 완료", type: "event", description: "카드 목록에서 DB 저장 완료 시 수신" }, ], }, touchOptimized: true, supportedDevices: ["mobile", "tablet"], });