"use client"; import { useCallback } 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 { usePopAction } from "@/hooks/pop/usePopAction"; import { usePopDesignerContext } from "@/components/pop/designer/PopDesignerContext"; import { Save, Trash2, LogOut, Menu, ExternalLink, Plus, Check, X, Edit, Search, RefreshCw, Download, Upload, Send, Copy, Settings, ChevronDown, 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" | "custom"; /** pop-button 전체 설정 */ export interface PopButtonConfig { label: string; variant: ButtonVariant; icon?: string; // Lucide 아이콘 이름 iconOnly?: boolean; preset: ButtonPreset; confirm?: ConfirmConfig; action: ButtonMainAction; followUpActions?: FollowUpAction[]; } // ======================================== // 상수 // ======================================== /** 메인 액션 타입 라벨 */ 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": "모달 열기", 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)" }, ]; /** 프리셋별 기본 설정 */ 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" }, }, 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}
); } /** 허용된 아이콘 맵 (개별 import로 트리 쉐이킹 적용) */ const LUCIDE_ICON_MAP: Record = { Save, Trash2, LogOut, Menu, ExternalLink, Plus, Check, X, Edit, Search, RefreshCw, Download, Upload, Send, Copy, Settings, ChevronDown, }; /** 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; } export function PopButtonComponent({ config, label, isDesignMode, screenId, }: PopButtonComponentProps) { // usePopAction 훅으로 액션 실행 통합 const { execute, isLoading, pendingConfirm, confirmExecute, cancelConfirm, } = usePopAction(screenId || ""); // 확인 메시지 결정 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]); // 클릭 핸들러 const handleClick = useCallback(async () => { // 디자인 모드: 실제 실행 안 함 if (isDesignMode) { toast.info( `[디자인 모드] ${ACTION_TYPE_LABELS[config?.action?.type || "save"]} 액션` ); return; } const action = config?.action; if (!action) return; await execute(action, { confirm: config?.confirm, followUpActions: config?.followUpActions, }); }, [isDesignMode, config?.action, config?.confirm, config?.followUpActions, execute]); // 외형 const buttonLabel = config?.label || label || "버튼"; const variant = config?.variant || "default"; const iconName = config?.icon || ""; const isIconOnly = config?.iconOnly || false; return ( <>
{/* 확인 다이얼로그 (usePopAction의 pendingConfirm 상태로 제어) */} { if (!open) cancelConfirm(); }}> 실행 확인 {getConfirmMessage()} 취소 확인 ); } // ======================================== // STEP 3: 설정 패널 // ======================================== interface PopButtonConfigPanelProps { config: PopButtonConfig; onUpdate: (config: PopButtonConfig) => void; } export function PopButtonConfigPanel({ config, onUpdate, }: PopButtonConfigPanelProps) { const isCustom = config?.preset === "custom"; // 프리셋 변경 핸들러 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 }) } />
{/* 메인 액션 */}
{/* 액션 타입 */}
{!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, touchOptimized: true, supportedDevices: ["mobile", "tablet"], });