import React from "react"; import DOMPurify from "isomorphic-dompurify"; import { Check, Save, CheckCircle, CircleCheck, FileCheck, ShieldCheck, Trash2, Trash, XCircle, X, Eraser, CircleX, Pencil, PenLine, Pen, SquarePen, FilePen, PenTool, ArrowRight, ExternalLink, MoveRight, Navigation, CornerUpRight, Link, Maximize2, PanelTop, AppWindow, LayoutGrid, Layers, FolderOpen, SendHorizontal, ArrowRightLeft, Repeat, PackageCheck, Upload, Share2, Download, FileDown, FileSpreadsheet, Sheet, Table, FileOutput, FileUp, FileInput, Zap, Plus, PlusCircle, SquarePlus, FilePlus, BadgePlus, Settings, SlidersHorizontal, ToggleLeft, Workflow, GitBranch, Settings2, ScanLine, QrCode, Camera, Scan, ScanBarcode, Focus, Truck, Car, MapPin, Navigation2, Route, Bell, Send, Radio, Megaphone, Podcast, BellRing, Copy, ClipboardCopy, Files, CopyPlus, ClipboardList, Clipboard, SquareMousePointer, type LucideIcon, } from "lucide-react"; // --------------------------------------------------------------------------- // 아이콘 이름 → 컴포넌트 매핑 (추천 아이콘만 명시적 import) // --------------------------------------------------------------------------- export const iconMap: Record = { Check, Save, CheckCircle, CircleCheck, FileCheck, ShieldCheck, Trash2, Trash, XCircle, X, Eraser, CircleX, Pencil, PenLine, Pen, SquarePen, FilePen, PenTool, ArrowRight, ExternalLink, MoveRight, Navigation, CornerUpRight, Link, Maximize2, PanelTop, AppWindow, LayoutGrid, Layers, FolderOpen, SendHorizontal, ArrowRightLeft, Repeat, PackageCheck, Upload, Share2, Download, FileDown, FileSpreadsheet, Sheet, Table, FileOutput, FileUp, FileInput, Zap, Plus, PlusCircle, SquarePlus, FilePlus, BadgePlus, Settings, SlidersHorizontal, ToggleLeft, Workflow, GitBranch, Settings2, ScanLine, QrCode, Camera, Scan, ScanBarcode, Focus, Truck, Car, MapPin, Navigation2, Route, Bell, Send, Radio, Megaphone, Podcast, BellRing, Copy, ClipboardCopy, Files, CopyPlus, ClipboardList, Clipboard, SquareMousePointer, }; // --------------------------------------------------------------------------- // 버튼 액션 → 추천 아이콘 이름 매핑 // --------------------------------------------------------------------------- export const actionIconMap: Record = { save: ["Check", "Save", "CheckCircle", "CircleCheck", "FileCheck", "ShieldCheck"], delete: ["Trash2", "Trash", "XCircle", "X", "Eraser", "CircleX"], edit: ["Pencil", "PenLine", "Pen", "SquarePen", "FilePen", "PenTool"], navigate: ["ArrowRight", "ExternalLink", "MoveRight", "Navigation", "CornerUpRight", "Link"], modal: ["Maximize2", "PanelTop", "AppWindow", "LayoutGrid", "Layers", "FolderOpen"], transferData: ["SendHorizontal", "ArrowRightLeft", "Repeat", "PackageCheck", "Upload", "Share2"], excel_download: ["Download", "FileDown", "FileSpreadsheet", "Sheet", "Table", "FileOutput"], excel_upload: ["Upload", "FileUp", "FileSpreadsheet", "Sheet", "FileInput", "FileOutput"], quickInsert: ["Zap", "Plus", "PlusCircle", "SquarePlus", "FilePlus", "BadgePlus"], control: ["Settings", "SlidersHorizontal", "ToggleLeft", "Workflow", "GitBranch", "Settings2"], barcode_scan: ["ScanLine", "QrCode", "Camera", "Scan", "ScanBarcode", "Focus"], operation_control: ["Truck", "Car", "MapPin", "Navigation2", "Route", "Bell"], event: ["Send", "Bell", "Radio", "Megaphone", "Podcast", "BellRing"], copy: ["Copy", "ClipboardCopy", "Files", "CopyPlus", "ClipboardList", "Clipboard"], }; // 아이콘 추천이 불가능한 deprecated/숨김 액션 export const noIconActions = new Set([ "openRelatedModal", "openModalWithData", "view_table_history", "code_merge", "empty_vehicle", ]); export const NO_ICON_MESSAGE = "적절한 추천 아이콘이 없습니다. 텍스트 모드를 사용하거나 아래에서 아이콘을 직접 추가하세요."; // 범용 폴백 아이콘 (추천 아이콘이 없는 액션용) export const FALLBACK_ICON_NAME = "SquareMousePointer"; /** 액션 타입에 대한 디폴트 아이콘(첫 번째 추천)을 반환. 없으면 범용 폴백. */ export function getDefaultIconForAction(actionType?: string): { name: string; type: "lucide" } { if (actionType && actionIconMap[actionType]?.length) { return { name: actionIconMap[actionType][0], type: "lucide" }; } return { name: FALLBACK_ICON_NAME, type: "lucide" }; } // --------------------------------------------------------------------------- // 아이콘 크기 (버튼 높이 대비 비율) // --------------------------------------------------------------------------- export const iconSizePresets: Record = { "작게": 40, "보통": 55, "크게": 70, "매우 크게": 85, }; /** 프리셋 문자열 → 비율(%) 반환. 레거시 값은 55(보통)로 폴백 */ export function getIconPercent(size: string | number): number { if (typeof size === "number") return size; return iconSizePresets[size] ?? 55; } /** 아이콘 크기를 CSS로 변환 (버튼 높이 대비 비율, 정사각형 유지) */ export function getIconSizeStyle(size: string | number): React.CSSProperties { const pct = getIconPercent(size); return { height: `${pct}%`, width: "auto", aspectRatio: "1 / 1" }; } // --------------------------------------------------------------------------- // 아이콘 조회 / 동적 등록 // --------------------------------------------------------------------------- export function getLucideIcon(name: string): LucideIcon | undefined { return iconMap[name]; } export function addToIconMap(name: string, component: LucideIcon): void { iconMap[name] = component; } // --------------------------------------------------------------------------- // SVG 정화 // --------------------------------------------------------------------------- export function sanitizeSvg(svgString: string): string { return DOMPurify.sanitize(svgString, { USE_PROFILES: { svg: true } }); } // --------------------------------------------------------------------------- // 버튼 아이콘 렌더러 컴포넌트 (모든 뷰어/위젯에서 공용) // --------------------------------------------------------------------------- export function ButtonIconRenderer({ componentConfig, fallbackLabel, }: { componentConfig: any; fallbackLabel: string; }) { const cfg = componentConfig || {}; const displayMode = cfg.displayMode || "text"; if (displayMode === "text" || !cfg.icon?.name) { return <>{cfg.text || fallbackLabel}; } return <>{getButtonDisplayContent(cfg)}; } // --------------------------------------------------------------------------- // 버튼 표시 콘텐츠 계산 (모든 렌더러 공용) // --------------------------------------------------------------------------- export function getButtonDisplayContent(componentConfig: any): React.ReactNode { const displayMode = componentConfig?.displayMode || "text"; const text = componentConfig?.text || componentConfig?.label || "버튼"; const icon = componentConfig?.icon; if (displayMode === "text" || !icon?.name) { return text; } // 아이콘 노드 생성 const sizeStyle = getIconSizeStyle(icon.size || "보통"); const colorStyle: React.CSSProperties = icon.color ? { color: icon.color } : {}; let iconNode: React.ReactNode = null; if (icon.type === "svg") { const svgIcon = componentConfig?.customSvgIcons?.find( (s: { name: string; svg: string }) => s.name === icon.name, ); if (svgIcon) { const clean = sanitizeSvg(svgIcon.svg); iconNode = ( ); } } else { const IconComponent = getLucideIcon(icon.name); if (IconComponent) { iconNode = ( ); } } if (!iconNode) { return text; } if (displayMode === "icon") { return iconNode; } // icon-text 모드 const gap = componentConfig?.iconGap ?? 6; const textPos = componentConfig?.iconTextPosition || "right"; const isVertical = textPos === "top" || textPos === "bottom"; const textFirst = textPos === "left" || textPos === "top"; return ( {textFirst ? {text} : iconNode} {textFirst ? iconNode : {text}} ); }