"use client"; import React, { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry"; import { GridMode } from "@/components/pop/designer/types/pop-layout"; import * as LucideIcons from "lucide-react"; import { toast } from "sonner"; // ======================================== // 타입 정의 // ======================================== export type IconType = "quick" | "emoji" | "image"; export type IconSizeMode = "auto" | "fixed"; export type LabelPosition = "bottom" | "right" | "none"; export type NavigateMode = "none" | "screen" | "url" | "back"; export interface IconSizeByMode { mobile_portrait: number; mobile_landscape: number; tablet_portrait: number; tablet_landscape: number; } export interface GradientConfig { from: string; to: string; direction?: "to-b" | "to-r" | "to-br"; } export interface ImageConfig { fileObjid?: number; imageUrl?: string; // 임시 저장용 (브라우저 캐시) tempDataUrl?: string; tempFileName?: string; } export interface PopIconAction { type: "navigate"; navigate: { mode: NavigateMode; screenId?: string; url?: string; }; } export interface QuickSelectItem { type: "lucide" | "emoji"; value: string; label: string; gradient: GradientConfig; } export interface PopIconConfig { iconType: IconType; // 빠른 선택용 quickSelectType?: "lucide" | "emoji"; quickSelectValue?: string; // 이미지용 imageConfig?: ImageConfig; imageScale?: number; // 공통 label?: string; labelPosition?: LabelPosition; labelColor?: string; labelFontSize?: number; backgroundColor?: string; gradient?: GradientConfig; borderRadiusPercent?: number; sizeMode: IconSizeMode; fixedSize?: number; sizeByMode?: IconSizeByMode; action: PopIconAction; } // ======================================== // 상수 // ======================================== export const ICON_TYPE_LABELS: Record = { quick: "빠른 선택", emoji: "이모지 직접 입력", image: "이미지", }; // 섹션 구분선 컴포넌트 function SectionDivider({ label }: { label: string }) { return (
{label}
); } export const NAVIGATE_MODE_LABELS: Record = { none: "없음", screen: "POP 화면", url: "외부 URL", back: "뒤로가기", }; export const LABEL_POSITION_LABELS: Record = { bottom: "아래", right: "오른쪽", none: "없음", }; export const DEFAULT_ICON_SIZE_BY_MODE: IconSizeByMode = { mobile_portrait: 48, mobile_landscape: 56, tablet_portrait: 64, tablet_landscape: 72, }; // 빠른 선택 아이템 (Lucide 10개 + 이모지) export const QUICK_SELECT_ITEMS: QuickSelectItem[] = [ // 기본 아이콘 (Lucide) - 10개 { type: "lucide", value: "Home", label: "홈", gradient: { from: "#0984e3", to: "#0774c4" } }, { type: "lucide", value: "ArrowLeft", label: "뒤로", gradient: { from: "#636e72", to: "#525d61" } }, { type: "lucide", value: "Settings", label: "설정", gradient: { from: "#636e72", to: "#525d61" } }, { type: "lucide", value: "Search", label: "검색", gradient: { from: "#fdcb6e", to: "#f9b93b" } }, { type: "lucide", value: "Plus", label: "추가", gradient: { from: "#00b894", to: "#00a86b" } }, { type: "lucide", value: "Check", label: "확인", gradient: { from: "#00b894", to: "#00a86b" } }, { type: "lucide", value: "X", label: "취소", gradient: { from: "#e17055", to: "#d35845" } }, { type: "lucide", value: "Edit", label: "수정", gradient: { from: "#0984e3", to: "#0774c4" } }, { type: "lucide", value: "Trash2", label: "삭제", gradient: { from: "#e17055", to: "#d35845" } }, { type: "lucide", value: "RefreshCw", label: "새로고침", gradient: { from: "#4ecdc4", to: "#26a69a" } }, // 이모지 { type: "emoji", value: "📋", label: "작업지시", gradient: { from: "#ff6b6b", to: "#ee5a5a" } }, { type: "emoji", value: "📊", label: "생산실적", gradient: { from: "#4ecdc4", to: "#26a69a" } }, { type: "emoji", value: "📦", label: "입고", gradient: { from: "#00b894", to: "#00a86b" } }, { type: "emoji", value: "🚚", label: "출고", gradient: { from: "#0984e3", to: "#0774c4" } }, { type: "emoji", value: "📈", label: "재고현황", gradient: { from: "#6c5ce7", to: "#5b4cdb" } }, { type: "emoji", value: "🔍", label: "품질검사", gradient: { from: "#fdcb6e", to: "#f9b93b" } }, { type: "emoji", value: "⚠️", label: "불량관리", gradient: { from: "#e17055", to: "#d35845" } }, { type: "emoji", value: "⚙️", label: "설비관리", gradient: { from: "#636e72", to: "#525d61" } }, { type: "emoji", value: "🦺", label: "안전관리", gradient: { from: "#f39c12", to: "#e67e22" } }, { type: "emoji", value: "🏭", label: "외주", gradient: { from: "#6c5ce7", to: "#5b4cdb" } }, { type: "emoji", value: "↩️", label: "반품", gradient: { from: "#e17055", to: "#d35845" } }, { type: "emoji", value: "🤝", label: "사급자재", gradient: { from: "#fdcb6e", to: "#f9b93b" } }, { type: "emoji", value: "🔄", label: "교환", gradient: { from: "#4ecdc4", to: "#26a69a" } }, { type: "emoji", value: "📍", label: "재고이동", gradient: { from: "#4ecdc4", to: "#26a69a" } }, ]; // ======================================== // 헬퍼 함수 // ======================================== function getIconSizeForMode(config: PopIconConfig | undefined, gridMode: GridMode): number { if (!config) return DEFAULT_ICON_SIZE_BY_MODE[gridMode]; if (config.sizeMode === "fixed" && config.fixedSize) { return config.fixedSize; } const sizes = config.sizeByMode || DEFAULT_ICON_SIZE_BY_MODE; return sizes[gridMode]; } function buildGradientStyle(gradient?: GradientConfig): React.CSSProperties { if (!gradient) return {}; const direction = gradient.direction || "to-b"; const dirMap: Record = { "to-b": "to bottom", "to-r": "to right", "to-br": "to bottom right" }; return { background: `linear-gradient(${dirMap[direction]}, ${gradient.from}, ${gradient.to})`, }; } function getImageUrl(imageConfig?: ImageConfig): string | undefined { if (!imageConfig) return undefined; // 임시 저장된 이미지 우선 if (imageConfig.tempDataUrl) return imageConfig.tempDataUrl; if (imageConfig.fileObjid) return `/api/files/preview/${imageConfig.fileObjid}`; return imageConfig.imageUrl; } // Lucide 아이콘 동적 렌더링 function DynamicLucideIcon({ name, size, 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 ; } // screenId에서 실제 ID만 추출 (URL이 입력된 경우 처리) function extractScreenId(input: string): string { if (!input) return ""; // URL 형태인 경우 (/pop/screens/123 또는 http://...pop/screens/123) const urlMatch = input.match(/\/pop\/screens\/(\d+)/); if (urlMatch) { return urlMatch[1]; } // http:// 또는 https://로 시작하는 경우 (다른 URL 형태) if (input.startsWith("http://") || input.startsWith("https://")) { // URL에서 마지막 숫자 부분 추출 시도 const lastNumberMatch = input.match(/\/(\d+)\/?$/); if (lastNumberMatch) { return lastNumberMatch[1]; } } // 숫자만 있는 경우 그대로 반환 if (/^\d+$/.test(input.trim())) { return input.trim(); } // 그 외의 경우 원본 반환 (에러 처리는 호출부에서) return input; } // ======================================== // 메인 컴포넌트 // ======================================== interface PopIconComponentProps { config?: PopIconConfig; label?: string; isDesignMode?: boolean; gridMode?: GridMode; } export function PopIconComponent({ config, label, isDesignMode, gridMode = "tablet_landscape" }: PopIconComponentProps) { const router = useRouter(); const iconType = config?.iconType || "quick"; const iconSize = getIconSizeForMode(config, gridMode); // 디자인 모드 확인 다이얼로그 상태 const [showNavigateDialog, setShowNavigateDialog] = useState(false); const [pendingNavigate, setPendingNavigate] = useState<{ mode: string; target: string } | null>(null); // 클릭 핸들러 const handleClick = () => { const navigate = config?.action?.navigate; if (!navigate || navigate.mode === "none") return; // 디자인 모드: 확인 다이얼로그 표시 if (isDesignMode) { if (navigate.mode === "screen") { if (!navigate.screenId) { toast.error("화면 ID가 설정되지 않았습니다."); return; } const cleanScreenId = extractScreenId(navigate.screenId); setPendingNavigate({ mode: "screen", target: cleanScreenId }); setShowNavigateDialog(true); } else if (navigate.mode === "url") { if (!navigate.url) { toast.error("URL이 설정되지 않았습니다."); return; } setPendingNavigate({ mode: "url", target: navigate.url }); setShowNavigateDialog(true); } else if (navigate.mode === "back") { toast.warning("뒤로가기는 실제 화면에서 테스트해주세요."); } return; } // 실제 모드: 직접 실행 switch (navigate.mode) { case "screen": if (navigate.screenId) { const cleanScreenId = extractScreenId(navigate.screenId); window.location.href = `/pop/screens/${cleanScreenId}`; } break; case "url": if (navigate.url) window.location.href = navigate.url; break; case "back": router.back(); break; } }; // 확인 후 이동 실행 const handleConfirmNavigate = () => { if (!pendingNavigate) return; if (pendingNavigate.mode === "screen") { const targetUrl = `/pop/screens/${pendingNavigate.target}`; console.log("[PopIcon] 화면 이동:", { target: pendingNavigate.target, url: targetUrl }); window.location.href = targetUrl; } else if (pendingNavigate.mode === "url") { console.log("[PopIcon] URL 이동:", pendingNavigate.target); window.location.href = pendingNavigate.target; } setShowNavigateDialog(false); setPendingNavigate(null); }; // 배경 스타일 (이미지 타입일 때는 배경 없음) const backgroundStyle: React.CSSProperties = iconType === "image" ? { backgroundColor: "transparent" } : config?.gradient ? buildGradientStyle(config.gradient) : { backgroundColor: config?.backgroundColor || "#e0e0e0" }; // 테두리 반경 (0% = 사각형, 100% = 원형) const radiusPercent = config?.borderRadiusPercent ?? 20; const borderRadius = iconType === "image" ? "0%" : `${radiusPercent / 2}%`; // 라벨 위치에 따른 레이아웃 const isLabelRight = config?.labelPosition === "right"; const showLabel = config?.labelPosition !== "none" && (config?.label || label); // 아이콘 렌더링 const renderIcon = () => { // 빠른 선택 if (iconType === "quick") { if (config?.quickSelectType === "lucide" && config?.quickSelectValue) { return ( ); } else if (config?.quickSelectType === "emoji" && config?.quickSelectValue) { return {config.quickSelectValue}; } // 기본값 return 📦; } // 이모지 직접 입력 if (iconType === "emoji") { if (config?.quickSelectValue) { return {config.quickSelectValue}; } return 📦; } // 이미지 (배경 없이 이미지만 표시) if (iconType === "image" && config?.imageConfig) { const scale = config?.imageScale || 100; return ( ); } return 📦; }; return (
{/* 아이콘 컨테이너 */}
{renderIcon()}
{/* 라벨 */} {showLabel && ( {config?.label || label} )} {/* 디자인 모드 네비게이션 확인 다이얼로그 */} 페이지 이동 확인 {pendingNavigate?.mode === "screen" ? "POP 화면으로 이동합니다." : "외부 URL로 이동합니다." }
※ 저장하지 않은 변경사항은 사라집니다.
확인 후 이동
); } // ======================================== // 설정 패널 // ======================================== interface PopIconConfigPanelProps { config: PopIconConfig; onUpdate: (config: PopIconConfig) => void; } export function PopIconConfigPanel({ config, onUpdate }: PopIconConfigPanelProps) { const iconType = config?.iconType || "quick"; return (
{/* 아이콘 타입 선택 */}
{/* 타입별 설정 */} {iconType === "quick" && } {iconType === "emoji" && } {iconType === "image" && } {/* 라벨 설정 */} {/* 스타일 설정 (이미지 타입 제외) */} {iconType !== "image" && ( <> )} {/* 액션 설정 */}
); } // 빠른 선택 그리드 function QuickSelectGrid({ config, onUpdate }: PopIconConfigPanelProps) { return (
{QUICK_SELECT_ITEMS.map((item, idx) => ( ))}
); } // 이모지 직접 입력 function EmojiInput({ config, onUpdate }: PopIconConfigPanelProps) { const [customEmoji, setCustomEmoji] = useState(config?.quickSelectValue || ""); const handleEmojiChange = (value: string) => { setCustomEmoji(value); // 이모지가 입력되면 바로 적용 if (value.trim()) { onUpdate({ ...config, quickSelectType: "emoji", quickSelectValue: value, gradient: config?.gradient || { from: "#6c5ce7", to: "#5b4cdb" }, }); } }; return (
handleEmojiChange(e.target.value)} placeholder="이모지를 입력하세요 (예: 📦, 🚀)" className="h-8 text-xs" maxLength={4} />

Windows: Win + . / Mac: Ctrl + Cmd + Space

{/* 배경 그라디언트 설정 */}
onUpdate({ ...config, gradient: { ...config?.gradient, from: e.target.value, to: config?.gradient?.to || "#5b4cdb" } })} className="h-8 w-full p-1 cursor-pointer" />
onUpdate({ ...config, gradient: { ...config?.gradient, from: config?.gradient?.from || "#6c5ce7", to: e.target.value } })} className="h-8 w-full p-1 cursor-pointer" />
{/* 미리보기 */} {customEmoji && (
{customEmoji}
)}
); } // 이미지 업로드 function ImageUpload({ config, onUpdate }: PopIconConfigPanelProps) { const [error, setError] = useState(null); const fileInputRef = useRef(null); // 파일 선택 시 브라우저 캐시에 임시 저장 (DB 업로드 X) const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setError(null); // 이미지 파일 검증 if (!file.type.startsWith("image/")) { setError("이미지 파일만 선택할 수 있습니다."); return; } // 파일 크기 제한 (5MB) if (file.size > 5 * 1024 * 1024) { setError("파일 크기는 5MB 이하여야 합니다."); return; } // FileReader로 Base64 변환 (브라우저 캐시) const reader = new FileReader(); reader.onload = () => { onUpdate({ ...config, imageConfig: { tempDataUrl: reader.result as string, tempFileName: file.name, // 기존 DB 파일 정보 제거 fileObjid: undefined, imageUrl: undefined, }, }); }; reader.onerror = () => { setError("파일을 읽는 중 오류가 발생했습니다."); }; reader.readAsDataURL(file); // input 초기화 (같은 파일 다시 선택 가능하도록) e.target.value = ""; }; // 이미지 삭제 const handleDelete = () => { onUpdate({ ...config, imageConfig: undefined, imageScale: undefined, }); }; // 미리보기 URL 가져오기 const getPreviewUrl = (): string | undefined => { if (config?.imageConfig?.tempDataUrl) return config.imageConfig.tempDataUrl; if (config?.imageConfig?.fileObjid) return `/api/files/preview/${config.imageConfig.fileObjid}`; return config?.imageConfig?.imageUrl; }; const previewUrl = getPreviewUrl(); const hasImage = !!previewUrl; const isTemp = !!config?.imageConfig?.tempDataUrl; return (
{/* 파일 선택 + 삭제 버튼 */}
{hasImage && ( )}
{/* 에러 메시지 */} {error &&

{error}

} {/* 또는 URL 직접 입력 */} onUpdate({ ...config, imageConfig: { imageUrl: e.target.value, // URL 입력 시 임시 파일 제거 tempDataUrl: undefined, tempFileName: undefined, fileObjid: undefined, } })} placeholder="또는 URL 직접 입력..." className="h-8 text-xs" disabled={isTemp} /> {/* 현재 이미지 미리보기 + 크기 조절 */} {hasImage && (
{/* eslint-disable-next-line @next/next/no-img-element */} 미리보기 {isTemp && ( 임시 )}
{config?.imageConfig?.tempFileName && (

{config.imageConfig.tempFileName}

)} onUpdate({ ...config, imageScale: Number(e.target.value) })} className="w-full" /> {isTemp && (

※ 화면 저장 시 서버에 업로드됩니다.

)}
)}
); } // 라벨 설정 function LabelSettings({ config, onUpdate }: PopIconConfigPanelProps) { return (
onUpdate({ ...config, label: e.target.value })} placeholder="라벨 텍스트" className="h-8 text-xs" />
onUpdate({ ...config, labelColor: e.target.value })} className="h-8 w-12 p-1 cursor-pointer" />
{/* 글자 크기 슬라이더 */} onUpdate({ ...config, labelFontSize: Number(e.target.value) })} className="w-full" />
); } // 스타일 설정 function StyleSettings({ config, onUpdate }: PopIconConfigPanelProps) { return (
onUpdate({ ...config, borderRadiusPercent: Number(e.target.value) })} className="w-full" />
); } // 액션 설정 function ActionSettings({ config, onUpdate }: PopIconConfigPanelProps) { const navigate = config?.action?.navigate || { mode: "none" as NavigateMode }; return (
{/* 없음이 아닐 때만 추가 설정 표시 */} {navigate.mode !== "none" && ( <> {navigate.mode === "screen" && ( onUpdate({ ...config, action: { type: "navigate", navigate: { ...navigate, screenId: e.target.value } } })} placeholder="화면 ID" className="h-8 text-xs mt-2" /> )} {navigate.mode === "url" && ( onUpdate({ ...config, action: { type: "navigate", navigate: { ...navigate, url: e.target.value } } })} placeholder="https://..." className="h-8 text-xs mt-2" /> )} {/* 테스트 버튼 */} )}
); } // ======================================== // 미리보기 컴포넌트 // ======================================== function PopIconPreviewComponent({ config }: { config?: PopIconConfig }) { return (
); } // ======================================== // 레지스트리 등록 // ======================================== PopComponentRegistry.registerComponent({ id: "pop-icon", name: "아이콘", description: "네비게이션 아이콘 (화면 이동, URL, 뒤로가기)", category: "action", icon: "MousePointer", component: PopIconComponent, configPanel: PopIconConfigPanel, preview: PopIconPreviewComponent, defaultProps: { iconType: "quick", quickSelectType: "emoji", quickSelectValue: "📦", label: "아이콘", labelPosition: "bottom", labelColor: "#000000", labelFontSize: 12, borderRadiusPercent: 20, sizeMode: "auto", action: { type: "navigate", navigate: { mode: "none" } }, } as PopIconConfig, touchOptimized: true, supportedDevices: ["mobile", "tablet"], });