ERP-node/frontend/lib/button-icon-map.tsx

214 lines
8.6 KiB
TypeScript
Raw Normal View History

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<string, LucideIcon> = {
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<string, string[]> = {
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<string, number> = {
"작게": 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 = (
<span
className="inline-flex items-center justify-center [&>svg]:h-full [&>svg]:w-full"
style={{ ...sizeStyle, ...colorStyle }}
dangerouslySetInnerHTML={{ __html: clean }}
/>
);
}
} else {
const IconComponent = getLucideIcon(icon.name);
if (IconComponent) {
iconNode = (
<span className="inline-flex items-center justify-center" style={sizeStyle}>
<IconComponent className="h-full w-full" style={colorStyle} />
</span>
);
}
}
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 (
<span
className="inline-flex items-center justify-center"
style={{
gap: `${gap}px`,
flexDirection: isVertical ? "column" : "row",
}}
>
{textFirst ? <span>{text}</span> : iconNode}
{textFirst ? iconNode : <span>{text}</span>}
</span>
);
}