diff --git a/POPUPDATE_2.md b/POPUPDATE_2.md index c4da5c4e..85e20af2 100644 --- a/POPUPDATE_2.md +++ b/POPUPDATE_2.md @@ -361,7 +361,7 @@ DashboardItem { - **한 줄 정의**: 누르면 어딘가로 이동 (돌아오는 값 없음) - **카테고리**: action -- **역할**: 네비게이션 (화면 이동, URL 이동, 새로고침) +- **역할**: 네비게이션 (화면 이동, URL 이동) - **데이터**: 없음 - **이벤트**: 없음 (네비게이션은 이벤트가 아닌 직접 실행) - **설정**: 아이콘 종류(lucide-icon), 라벨, 배경색/그라디언트, 크기, 클릭 액션(PopActionConfig), 뱃지 표시 diff --git a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx index 0a9d6037..847d8aed 100644 --- a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx +++ b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx @@ -109,8 +109,8 @@ export default function ComponentEditorPanel({ {/* 탭 */} - - + + 위치 @@ -130,7 +130,7 @@ export default function ComponentEditorPanel({ {/* 위치 탭 */} - + {/* 배치된 컴포넌트 목록 */} {allComponents && allComponents.length > 0 && (
@@ -178,7 +178,7 @@ export default function ComponentEditorPanel({ {/* 설정 탭 */} - + {/* 표시 탭 */} - + {/* 데이터 탭 */} - + diff --git a/frontend/components/pop/designer/panels/ComponentPalette.tsx b/frontend/components/pop/designer/panels/ComponentPalette.tsx index 05db0aab..a0d628aa 100644 --- a/frontend/components/pop/designer/panels/ComponentPalette.tsx +++ b/frontend/components/pop/designer/panels/ComponentPalette.tsx @@ -3,7 +3,7 @@ import { useDrag } from "react-dnd"; import { cn } from "@/lib/utils"; import { PopComponentType } from "../types/pop-layout"; -import { Square, FileText } from "lucide-react"; +import { Square, FileText, MousePointer } from "lucide-react"; import { DND_ITEM_TYPES } from "../constants"; // 컴포넌트 정의 @@ -27,6 +27,12 @@ const PALETTE_ITEMS: PaletteItem[] = [ icon: FileText, description: "텍스트, 시간, 이미지 표시", }, + { + type: "pop-icon", + label: "아이콘", + icon: MousePointer, + description: "네비게이션 아이콘 (화면 이동, URL, 뒤로가기)", + }, ]; // 드래그 가능한 컴포넌트 아이템 diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 633dbf5e..7f5a570b 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -54,6 +54,8 @@ interface PopRendererProps { overridePadding?: number; /** 추가 className */ className?: string; + /** 현재 편집 중인 화면 ID (아이콘 네비게이션용) */ + currentScreenId?: number; } // ======================================== @@ -83,6 +85,7 @@ export default function PopRenderer({ overrideGap, overridePadding, className, + currentScreenId, }: PopRendererProps) { const { gridConfig, components, overrides } = layout; @@ -511,9 +514,19 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect // 실제 컴포넌트가 등록되어 있으면 실제 데이터로 렌더링 (대시보드 등) if (ActualComp) { + // 아이콘 컴포넌트는 클릭 이벤트가 필요하므로 pointer-events 허용 + const needsPointerEvents = component.type === "pop-icon"; + return ( -
- +
+
); } diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 1a8335ec..f92791c9 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -9,7 +9,7 @@ /** * POP 컴포넌트 타입 */ -export type PopComponentType = "pop-sample" | "pop-text"; // 테스트용 샘플 박스, 텍스트 컴포넌트 +export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon"; // 테스트용 샘플 박스, 텍스트 컴포넌트, 아이콘 컴포넌트 /** * 데이터 흐름 정의 @@ -342,6 +342,7 @@ export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => { export const DEFAULT_COMPONENT_GRID_SIZE: Record = { "pop-sample": { colSpan: 2, rowSpan: 1 }, "pop-text": { colSpan: 3, rowSpan: 1 }, + "pop-icon": { colSpan: 1, rowSpan: 2 }, }; /** diff --git a/frontend/lib/registry/pop-components/index.ts b/frontend/lib/registry/pop-components/index.ts index b604f9e8..c73df551 100644 --- a/frontend/lib/registry/pop-components/index.ts +++ b/frontend/lib/registry/pop-components/index.ts @@ -13,6 +13,7 @@ export * from "./types"; // POP 컴포넌트 등록 import "./pop-text"; +import "./pop-icon"; // 향후 추가될 컴포넌트들: // import "./pop-field"; diff --git a/frontend/lib/registry/pop-components/pop-icon.tsx b/frontend/lib/registry/pop-components/pop-icon.tsx new file mode 100644 index 00000000..1d61afd9 --- /dev/null +++ b/frontend/lib/registry/pop-components/pop-icon.tsx @@ -0,0 +1,974 @@ +"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"], +}); diff --git a/frontend/lib/registry/pop-components/pop-text.tsx b/frontend/lib/registry/pop-components/pop-text.tsx index 8cad19ad..dd51a158 100644 --- a/frontend/lib/registry/pop-components/pop-text.tsx +++ b/frontend/lib/registry/pop-components/pop-text.tsx @@ -391,6 +391,19 @@ interface PopTextConfigPanelProps { onUpdate: (config: PopTextConfig) => void; } +// 섹션 구분선 컴포넌트 +function SectionDivider({ label }: { label: string }) { + return ( +
+
+
+ {label} +
+
+
+ ); +} + export function PopTextConfigPanel({ config, onUpdate, @@ -398,10 +411,10 @@ export function PopTextConfigPanel({ const textType = config?.textType || "text"; return ( -
+
{/* 텍스트 타입 선택 */} +
-