"use client"; import { useState, 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 { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useDataSource } from "@/hooks/pop/useDataSource"; import * as LucideIcons from "lucide-react"; import { toast } from "sonner"; // ======================================== // STEP 1: 타입 정의 // ======================================== /** 메인 액션 타입 (5종) */ export type ButtonActionType = "save" | "delete" | "api" | "modal" | "event"; /** 후속 액션 타입 (3종) */ export type FollowUpActionType = "event" | "refresh" | "navigate"; /** 버튼 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: "화면 이동", }; /** 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}
); } /** Lucide 아이콘 동적 렌더링 */ function DynamicLucideIcon({ name, size = 16, className, }: { name: string; size?: number; className?: string; }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const IconComponent = (LucideIcons as any)[name]; if (!IconComponent) return null; return ; } // ======================================== // STEP 2: 메인 컴포넌트 // ======================================== interface PopButtonComponentProps { config?: PopButtonConfig; label?: string; isDesignMode?: boolean; } export function PopButtonComponent({ config, label, isDesignMode, }: PopButtonComponentProps) { const [showConfirm, setShowConfirm] = useState(false); // 이벤트 훅 (1차: screenId 빈 문자열 - 후속 통합에서 주입) const { publish } = usePopEvent(""); // 데이터 훅 (save/delete용, tableName 없으면 자동 스킵) const { save, remove, refetch } = useDataSource({ tableName: config?.action?.targetTable || "", }); // 확인 메시지 결정 const getConfirmMessage = useCallback((): string => { if (config?.confirm?.message) return config.confirm.message; return DEFAULT_CONFIRM_MESSAGES[config?.action?.type || "save"]; }, [config?.confirm?.message, config?.action?.type]); // 메인 액션 실행 const executeMainAction = useCallback(async (): Promise => { const action = config?.action; if (!action) return false; try { switch (action.type) { case "save": { // sharedData에서 데이터 수집 (후속 통합에서 실제 구현) const record: Record = {}; const result = await save(record); if (!result.success) { toast.error(result.error || "저장 실패"); return false; } toast.success("저장되었습니다"); return true; } case "delete": { // sharedData에서 ID 수집 (후속 통합에서 실제 구현) const result = await remove(""); if (!result.success) { toast.error(result.error || "삭제 실패"); return false; } toast.success("삭제되었습니다"); return true; } case "api": { // 1차: toast 알림만 (실제 API 호출은 후속) toast.info( `API 호출 예정: ${action.apiMethod || "POST"} ${action.apiEndpoint || "(미설정)"}` ); return true; } case "modal": { // 1차: toast 알림만 (실제 모달은 후속) toast.info( `모달 열기 예정: ${MODAL_MODE_LABELS[action.modalMode || "fullscreen"]}` ); return true; } case "event": { if (action.eventName) { publish(action.eventName, action.eventPayload); toast.success(`이벤트 발행: ${action.eventName}`); } return true; } default: return false; } } catch (err: unknown) { const message = err instanceof Error ? err.message : "액션 실행 실패"; toast.error(message); return false; } }, [config?.action, save, remove, publish]); // 후속 액션 순차 실행 const executeFollowUpActions = useCallback(async () => { const actions = config?.followUpActions; if (!actions || actions.length === 0) return; for (const fa of actions) { try { switch (fa.type) { case "event": if (fa.eventName) { publish(fa.eventName, fa.eventPayload); } break; case "refresh": publish("__refresh__"); await refetch(); break; case "navigate": if (fa.targetScreenId) { window.location.href = `/pop/screens/${fa.targetScreenId}`; return; // navigate 후 중단 } break; } } catch (err: unknown) { const message = err instanceof Error ? err.message : "후속 액션 실패"; toast.error(message); // 개별 실패 시 다음 진행 } } }, [config?.followUpActions, publish, refetch]); // 클릭 핸들러 const handleClick = useCallback(async () => { // 디자인 모드: 실제 실행 안 함 if (isDesignMode) { toast.info( `[디자인 모드] ${ACTION_TYPE_LABELS[config?.action?.type || "save"]} 액션` ); return; } // 확인 다이얼로그 필요 시 if (config?.confirm?.enabled) { setShowConfirm(true); return; } // 바로 실행 const success = await executeMainAction(); if (success) { await executeFollowUpActions(); } }, [ isDesignMode, config?.confirm?.enabled, config?.action?.type, executeMainAction, executeFollowUpActions, ]); // 확인 후 실행 const handleConfirmExecute = useCallback(async () => { setShowConfirm(false); const success = await executeMainAction(); if (success) { await executeFollowUpActions(); } }, [executeMainAction, executeFollowUpActions]); // 외형 const buttonLabel = config?.label || label || "버튼"; const variant = config?.variant || "default"; const iconName = config?.icon || ""; const isIconOnly = config?.iconOnly || false; return ( <>
{/* 확인 다이얼로그 */} 실행 확인 {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; }) { 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} />
); 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"], });