Compare commits
2 Commits
134976ff9e
...
49e8e40521
| Author | SHA1 | Date |
|---|---|---|
|
|
49e8e40521 | |
|
|
b071d8090b |
|
|
@ -11,6 +11,7 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere
|
||||||
import { DynamicWebTypeRenderer } from "@/lib/registry";
|
import { DynamicWebTypeRenderer } from "@/lib/registry";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { initializeComponents } from "@/lib/registry/components";
|
||||||
|
|
||||||
export default function ScreenViewPage() {
|
export default function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -23,6 +24,20 @@ export default function ScreenViewPage() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initComponents = async () => {
|
||||||
|
try {
|
||||||
|
console.log("🚀 할당된 화면에서 컴포넌트 시스템 초기화 시작...");
|
||||||
|
await initializeComponents();
|
||||||
|
console.log("✅ 할당된 화면에서 컴포넌트 시스템 초기화 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initComponents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadScreen = async () => {
|
const loadScreen = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -150,10 +165,19 @@ export default function ScreenViewPage() {
|
||||||
allComponents={layout.components}
|
allComponents={layout.components}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={(fieldName, value) => {
|
onFormDataChange={(fieldName, value) => {
|
||||||
setFormData((prev) => ({
|
console.log("📝 폼 데이터 변경:", { fieldName, value });
|
||||||
...prev,
|
setFormData((prev) => {
|
||||||
[fieldName]: value,
|
const newFormData = {
|
||||||
}));
|
...prev,
|
||||||
|
[fieldName]: value,
|
||||||
|
};
|
||||||
|
console.log("📊 전체 폼 데이터:", newFormData);
|
||||||
|
return newFormData;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
screenInfo={{
|
||||||
|
id: screenId,
|
||||||
|
tableName: screen?.tableName,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -234,6 +258,14 @@ export default function ScreenViewPage() {
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
|
screenId={screenId}
|
||||||
|
tableName={screen?.tableName}
|
||||||
|
onRefresh={() => {
|
||||||
|
console.log("화면 새로고침 요청");
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
console.log("화면 닫기 요청");
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DynamicWebTypeRenderer
|
<DynamicWebTypeRenderer
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { Inter, JetBrains_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { QueryProvider } from "@/providers/QueryProvider";
|
import { QueryProvider } from "@/providers/QueryProvider";
|
||||||
import { RegistryProvider } from "./registry-provider";
|
import { RegistryProvider } from "./registry-provider";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
import ScreenModal from "@/components/common/ScreenModal";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
|
@ -44,6 +46,8 @@ export default function RootLayout({
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<RegistryProvider>{children}</RegistryProvider>
|
<RegistryProvider>{children}</RegistryProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
|
<Toaster position="top-right" richColors />
|
||||||
|
<ScreenModal />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||||
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
import { ComponentData } from "@/types/screen";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface ScreenModalState {
|
||||||
|
isOpen: boolean;
|
||||||
|
screenId: number | null;
|
||||||
|
title: string;
|
||||||
|
size: "sm" | "md" | "lg" | "xl";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScreenModalProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
|
const [modalState, setModalState] = useState<ScreenModalState>({
|
||||||
|
isOpen: false,
|
||||||
|
screenId: null,
|
||||||
|
title: "",
|
||||||
|
size: "md",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [screenData, setScreenData] = useState<{
|
||||||
|
components: ComponentData[];
|
||||||
|
screenInfo: any;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [screenDimensions, setScreenDimensions] = useState<{
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// 화면의 실제 크기 계산 함수
|
||||||
|
const calculateScreenDimensions = (components: ComponentData[]) => {
|
||||||
|
let maxWidth = 800; // 최소 너비
|
||||||
|
let maxHeight = 600; // 최소 높이
|
||||||
|
|
||||||
|
components.forEach((component) => {
|
||||||
|
const x = parseFloat(component.style?.positionX || "0");
|
||||||
|
const y = parseFloat(component.style?.positionY || "0");
|
||||||
|
const width = parseFloat(component.style?.width || "100");
|
||||||
|
const height = parseFloat(component.style?.height || "40");
|
||||||
|
|
||||||
|
// 컴포넌트의 오른쪽 끝과 아래쪽 끝 계산
|
||||||
|
const rightEdge = x + width;
|
||||||
|
const bottomEdge = y + height;
|
||||||
|
|
||||||
|
maxWidth = Math.max(maxWidth, rightEdge + 50); // 여백 추가
|
||||||
|
maxHeight = Math.max(maxHeight, bottomEdge + 50); // 여백 추가
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: Math.min(maxWidth, window.innerWidth * 0.9), // 화면의 90%를 넘지 않도록
|
||||||
|
height: Math.min(maxHeight, window.innerHeight * 0.8), // 화면의 80%를 넘지 않도록
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전역 모달 이벤트 리스너
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOpenModal = (event: CustomEvent) => {
|
||||||
|
const { screenId, title, size } = event.detail;
|
||||||
|
setModalState({
|
||||||
|
isOpen: true,
|
||||||
|
screenId,
|
||||||
|
title,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("openScreenModal", handleOpenModal as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("openScreenModal", handleOpenModal as EventListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 화면 데이터 로딩
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalState.isOpen && modalState.screenId) {
|
||||||
|
loadScreenData(modalState.screenId);
|
||||||
|
}
|
||||||
|
}, [modalState.isOpen, modalState.screenId]);
|
||||||
|
|
||||||
|
const loadScreenData = async (screenId: number) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
console.log("화면 데이터 로딩 시작:", screenId);
|
||||||
|
|
||||||
|
// 화면 정보와 레이아웃 데이터 로딩
|
||||||
|
const [screenInfo, layoutData] = await Promise.all([
|
||||||
|
screenApi.getScreen(screenId),
|
||||||
|
screenApi.getLayout(screenId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("API 응답:", { screenInfo, layoutData });
|
||||||
|
|
||||||
|
// screenApi는 직접 데이터를 반환하므로 .success 체크 불필요
|
||||||
|
if (screenInfo && layoutData) {
|
||||||
|
const components = layoutData.components || [];
|
||||||
|
|
||||||
|
// 화면의 실제 크기 계산
|
||||||
|
const dimensions = calculateScreenDimensions(components);
|
||||||
|
setScreenDimensions(dimensions);
|
||||||
|
|
||||||
|
setScreenData({
|
||||||
|
components,
|
||||||
|
screenInfo: screenInfo,
|
||||||
|
});
|
||||||
|
console.log("화면 데이터 설정 완료:", {
|
||||||
|
componentsCount: components.length,
|
||||||
|
dimensions,
|
||||||
|
screenInfo,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("화면 데이터가 없습니다");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("화면 데이터 로딩 오류:", error);
|
||||||
|
toast.error("화면을 불러오는 중 오류가 발생했습니다.");
|
||||||
|
handleClose();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setModalState({
|
||||||
|
isOpen: false,
|
||||||
|
screenId: null,
|
||||||
|
title: "",
|
||||||
|
size: "md",
|
||||||
|
});
|
||||||
|
setScreenData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 크기 설정 - 화면 내용에 맞게 동적 조정
|
||||||
|
const getModalStyle = () => {
|
||||||
|
if (!screenDimensions) {
|
||||||
|
return {
|
||||||
|
className: "w-fit min-w-[400px] max-w-4xl max-h-[80vh] overflow-hidden",
|
||||||
|
style: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 헤더 높이와 패딩을 고려한 전체 높이 계산
|
||||||
|
const headerHeight = 60; // DialogHeader + 패딩
|
||||||
|
const totalHeight = screenDimensions.height + headerHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
className: "overflow-hidden p-0",
|
||||||
|
style: {
|
||||||
|
width: `${screenDimensions.width + 48}px`, // 헤더 패딩과 여백 고려
|
||||||
|
height: `${Math.min(totalHeight, window.innerHeight * 0.8)}px`,
|
||||||
|
maxWidth: '90vw',
|
||||||
|
maxHeight: '80vh'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalStyle = getModalStyle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent
|
||||||
|
className={`${modalStyle.className} ${className || ""}`}
|
||||||
|
style={modalStyle.style}
|
||||||
|
>
|
||||||
|
<DialogHeader className="px-6 py-4 border-b">
|
||||||
|
<DialogTitle>{modalState.title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden p-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">화면을 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : screenData ? (
|
||||||
|
<div
|
||||||
|
className="relative bg-white overflow-hidden"
|
||||||
|
style={{
|
||||||
|
width: (screenDimensions?.width || 800),
|
||||||
|
height: (screenDimensions?.height || 600),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{screenData.components.map((component) => (
|
||||||
|
<InteractiveScreenViewerDynamic
|
||||||
|
key={component.id}
|
||||||
|
component={component}
|
||||||
|
allComponents={screenData.components}
|
||||||
|
screenInfo={{
|
||||||
|
id: modalState.screenId!,
|
||||||
|
tableName: screenData.screenInfo?.tableName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<p className="text-gray-600">화면 데이터가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScreenModal;
|
||||||
|
|
@ -165,6 +165,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
componentId: comp.id,
|
componentId: comp.id,
|
||||||
componentType: comp.type,
|
componentType: comp.type,
|
||||||
componentConfig: comp.componentConfig,
|
componentConfig: comp.componentConfig,
|
||||||
|
style: comp.style,
|
||||||
|
size: comp.size,
|
||||||
|
position: comp.position,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -173,6 +176,21 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFormDataChange={handleFormDataChange}
|
onFormDataChange={handleFormDataChange}
|
||||||
|
screenId={screenInfo?.id}
|
||||||
|
tableName={screenInfo?.tableName}
|
||||||
|
onRefresh={() => {
|
||||||
|
// 화면 새로고침 로직 (필요시 구현)
|
||||||
|
console.log("화면 새로고침 요청");
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
// 화면 닫기 로직 (필요시 구현)
|
||||||
|
console.log("화면 닫기 요청");
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
...comp.style,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import "@/lib/registry/components";
|
||||||
interface RealtimePreviewProps {
|
interface RealtimePreviewProps {
|
||||||
component: ComponentData;
|
component: ComponentData;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
isDesignMode?: boolean; // 편집 모드 여부
|
||||||
onClick?: (e?: React.MouseEvent) => void;
|
onClick?: (e?: React.MouseEvent) => void;
|
||||||
onDragStart?: (e: React.DragEvent) => void;
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
|
|
@ -63,6 +64,7 @@ const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => {
|
||||||
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
component,
|
component,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isDesignMode = true, // 기본값은 편집 모드
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
|
|
@ -122,6 +124,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
isDesignMode={isDesignMode}
|
||||||
|
isInteractive={!isDesignMode} // 편집 모드가 아닐 때만 인터랙티브
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
SCREEN_RESOLUTIONS,
|
SCREEN_RESOLUTIONS,
|
||||||
} from "@/types/screen";
|
} from "@/types/screen";
|
||||||
import { generateComponentId } from "@/lib/utils/generateId";
|
import { generateComponentId } from "@/lib/utils/generateId";
|
||||||
|
import { getComponentIdFromWebType } from "@/lib/utils/webTypeMapping";
|
||||||
import {
|
import {
|
||||||
createGroupComponent,
|
createGroupComponent,
|
||||||
calculateBoundingBox,
|
calculateBoundingBox,
|
||||||
|
|
@ -38,6 +39,7 @@ import { GroupingToolbar } from "./GroupingToolbar";
|
||||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||||
|
import { initializeComponents } from "@/lib/registry/components";
|
||||||
|
|
||||||
import StyleEditor from "./StyleEditor";
|
import StyleEditor from "./StyleEditor";
|
||||||
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
import { RealtimePreview } from "./RealtimePreviewDynamic";
|
||||||
|
|
@ -629,6 +631,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
[layout, gridInfo, saveToHistory],
|
[layout, gridInfo, saveToHistory],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 컴포넌트 시스템 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
const initComponents = async () => {
|
||||||
|
try {
|
||||||
|
console.log("🚀 컴포넌트 시스템 초기화 시작...");
|
||||||
|
await initializeComponents();
|
||||||
|
console.log("✅ 컴포넌트 시스템 초기화 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 컴포넌트 시스템 초기화 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initComponents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 테이블 데이터 로드 (성능 최적화: 선택된 테이블만 조회)
|
// 테이블 데이터 로드 (성능 최적화: 선택된 테이블만 조회)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScreen?.tableName && selectedScreen.tableName.trim()) {
|
if (selectedScreen?.tableName && selectedScreen.tableName.trim()) {
|
||||||
|
|
@ -1499,7 +1516,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
|
|
||||||
const newComponent: ComponentData = {
|
const newComponent: ComponentData = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "widget", // 새 컴포넌트는 모두 widget 타입
|
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
||||||
label: component.name,
|
label: component.name,
|
||||||
widgetType: component.webType,
|
widgetType: component.webType,
|
||||||
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
||||||
|
|
@ -1507,11 +1524,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
size: component.defaultSize,
|
size: component.defaultSize,
|
||||||
componentConfig: {
|
componentConfig: {
|
||||||
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
||||||
|
webType: component.webType, // 웹타입 정보 추가
|
||||||
...component.defaultConfig,
|
...component.defaultConfig,
|
||||||
},
|
},
|
||||||
webTypeConfig: getDefaultWebTypeConfig(component.webType),
|
webTypeConfig: getDefaultWebTypeConfig(component.webType),
|
||||||
style: {
|
style: {
|
||||||
labelDisplay: true,
|
labelDisplay: component.id === "text-display" ? false : true, // 텍스트 표시 컴포넌트는 기본적으로 라벨 숨김
|
||||||
labelFontSize: "14px",
|
labelFontSize: "14px",
|
||||||
labelColor: "#374151",
|
labelColor: "#374151",
|
||||||
labelFontWeight: "500",
|
labelFontWeight: "500",
|
||||||
|
|
@ -1547,10 +1565,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const dragData = e.dataTransfer.getData("application/json");
|
const dragData = e.dataTransfer.getData("application/json");
|
||||||
if (!dragData) return;
|
console.log("🎯 드롭 이벤트:", { dragData });
|
||||||
|
if (!dragData) {
|
||||||
|
console.log("❌ 드래그 데이터가 없습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(dragData);
|
const parsedData = JSON.parse(dragData);
|
||||||
|
console.log("📋 파싱된 데이터:", parsedData);
|
||||||
|
|
||||||
// 템플릿 드래그인 경우
|
// 템플릿 드래그인 경우
|
||||||
if (parsedData.type === "template") {
|
if (parsedData.type === "template") {
|
||||||
|
|
@ -1603,6 +1626,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else if (type === "column") {
|
} else if (type === "column") {
|
||||||
|
console.log("🔄 컬럼 드롭 처리:", { webType: column.widgetType, columnName: column.columnName });
|
||||||
// 현재 해상도에 맞는 격자 정보로 기본 크기 계산
|
// 현재 해상도에 맞는 격자 정보로 기본 크기 계산
|
||||||
const currentGridInfo = layout.gridSettings
|
const currentGridInfo = layout.gridSettings
|
||||||
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
? calculateGridInfo(screenResolution.width, screenResolution.height, {
|
||||||
|
|
@ -1767,18 +1791,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const relativeX = e.clientX - containerRect.left;
|
const relativeX = e.clientX - containerRect.left;
|
||||||
const relativeY = e.clientY - containerRect.top;
|
const relativeY = e.clientY - containerRect.top;
|
||||||
|
|
||||||
|
// 웹타입을 새로운 컴포넌트 ID로 매핑
|
||||||
|
const componentId = getComponentIdFromWebType(column.widgetType);
|
||||||
|
console.log(`🔄 폼 컨테이너 드롭: ${column.widgetType} → ${componentId}`);
|
||||||
|
|
||||||
newComponent = {
|
newComponent = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "widget",
|
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
||||||
label: column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
widgetType: column.widgetType,
|
|
||||||
required: column.required,
|
required: column.required,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
parentId: formContainerId, // 폼 컨테이너의 자식으로 설정
|
||||||
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
position: { x: relativeX, y: relativeY, z: 1 } as Position,
|
||||||
size: { width: defaultWidth, height: 40 },
|
size: { width: defaultWidth, height: 40 },
|
||||||
|
gridColumns: 1,
|
||||||
style: {
|
style: {
|
||||||
labelDisplay: true,
|
labelDisplay: true,
|
||||||
labelFontSize: "12px",
|
labelFontSize: "12px",
|
||||||
|
|
@ -1786,20 +1814,26 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
labelFontWeight: "500",
|
labelFontWeight: "500",
|
||||||
labelMarginBottom: "6px",
|
labelMarginBottom: "6px",
|
||||||
},
|
},
|
||||||
webTypeConfig: getDefaultWebTypeConfig(column.widgetType),
|
componentConfig: {
|
||||||
|
type: componentId, // text-input, number-input 등
|
||||||
|
webType: column.widgetType, // 원본 웹타입 보존
|
||||||
|
...getDefaultWebTypeConfig(column.widgetType),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return; // 폼 컨테이너를 찾을 수 없으면 드롭 취소
|
return; // 폼 컨테이너를 찾을 수 없으면 드롭 취소
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 일반 캔버스에 드롭한 경우 (기존 로직)
|
// 일반 캔버스에 드롭한 경우 - 새로운 컴포넌트 시스템 사용
|
||||||
|
const componentId = getComponentIdFromWebType(column.widgetType);
|
||||||
|
console.log(`🔄 캔버스 드롭: ${column.widgetType} → ${componentId}`);
|
||||||
|
|
||||||
newComponent = {
|
newComponent = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "widget",
|
type: "component", // ✅ 새로운 컴포넌트 시스템 사용
|
||||||
label: column.columnLabel || column.columnName, // 컬럼 라벨 우선, 없으면 컬럼명
|
label: column.columnLabel || column.columnName, // 컬럼 라벨 우선, 없으면 컬럼명
|
||||||
tableName: table.tableName,
|
tableName: table.tableName,
|
||||||
columnName: column.columnName,
|
columnName: column.columnName,
|
||||||
widgetType: column.widgetType,
|
|
||||||
required: column.required,
|
required: column.required,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
position: { x, y, z: 1 } as Position,
|
position: { x, y, z: 1 } as Position,
|
||||||
|
|
@ -1812,7 +1846,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
labelFontWeight: "500",
|
labelFontWeight: "500",
|
||||||
labelMarginBottom: "6px",
|
labelMarginBottom: "6px",
|
||||||
},
|
},
|
||||||
webTypeConfig: getDefaultWebTypeConfig(column.widgetType),
|
componentConfig: {
|
||||||
|
type: componentId, // text-input, number-input 등
|
||||||
|
webType: column.widgetType, // 원본 웹타입 보존
|
||||||
|
...getDefaultWebTypeConfig(column.widgetType),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3093,7 +3131,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("🎯 캔버스 드롭 이벤트 발생");
|
console.log("🎯 캔버스 드롭 이벤트 발생");
|
||||||
handleComponentDrop(e);
|
handleDrop(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 격자 라인 */}
|
{/* 격자 라인 */}
|
||||||
|
|
@ -3176,6 +3214,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
isSelected={
|
isSelected={
|
||||||
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
|
selectedComponent?.id === component.id || groupState.selectedComponents.includes(component.id)
|
||||||
}
|
}
|
||||||
|
isDesignMode={true} // 편집 모드로 설정
|
||||||
onClick={(e) => handleComponentClick(component, e)}
|
onClick={(e) => handleComponentClick(component, e)}
|
||||||
onDragStart={(e) => startComponentDrag(component, e)}
|
onDragStart={(e) => startComponentDrag(component, e)}
|
||||||
onDragEnd={endDrag}
|
onDragEnd={endDrag}
|
||||||
|
|
@ -3255,6 +3294,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
isSelected={
|
isSelected={
|
||||||
selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
|
selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id)
|
||||||
}
|
}
|
||||||
|
isDesignMode={true} // 편집 모드로 설정
|
||||||
onClick={(e) => handleComponentClick(child, e)}
|
onClick={(e) => handleComponentClick(child, e)}
|
||||||
onDragStart={(e) => startComponentDrag(child, e)}
|
onDragStart={(e) => startComponentDrag(child, e)}
|
||||||
onDragEnd={endDrag}
|
onDragEnd={endDrag}
|
||||||
|
|
@ -3323,11 +3363,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
onSearchChange={setSearchTerm}
|
onSearchChange={setSearchTerm}
|
||||||
onDragStart={(e, table, column) => {
|
onDragStart={(e, table, column) => {
|
||||||
|
console.log("🚀 드래그 시작:", { table: table.tableName, column: column?.columnName });
|
||||||
const dragData = {
|
const dragData = {
|
||||||
type: column ? "column" : "table",
|
type: column ? "column" : "table",
|
||||||
table,
|
table,
|
||||||
column,
|
column,
|
||||||
};
|
};
|
||||||
|
console.log("📦 드래그 데이터:", dragData);
|
||||||
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
||||||
}}
|
}}
|
||||||
selectedTableName={selectedScreen.tableName}
|
selectedTableName={selectedScreen.tableName}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,82 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Check, ChevronsUpDown, Search } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { ComponentData } from "@/types/screen";
|
import { ComponentData } from "@/types/screen";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface ButtonConfigPanelProps {
|
interface ButtonConfigPanelProps {
|
||||||
component: ComponentData;
|
component: ComponentData;
|
||||||
onUpdateProperty: (path: string, value: any) => void;
|
onUpdateProperty: (path: string, value: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScreenOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
|
||||||
const config = component.componentConfig || {};
|
const config = component.componentConfig || {};
|
||||||
|
const [screens, setScreens] = useState<ScreenOption[]>([]);
|
||||||
|
const [screensLoading, setScreensLoading] = useState(false);
|
||||||
|
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
||||||
|
const [navScreenOpen, setNavScreenOpen] = useState(false);
|
||||||
|
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||||
|
const [navSearchTerm, setNavSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// 화면 목록 가져오기
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchScreens = async () => {
|
||||||
|
try {
|
||||||
|
setScreensLoading(true);
|
||||||
|
console.log("🔍 화면 목록 API 호출 시작");
|
||||||
|
const response = await apiClient.get("/screen-management/screens");
|
||||||
|
console.log("✅ 화면 목록 API 응답:", response.data);
|
||||||
|
|
||||||
|
if (response.data.success && Array.isArray(response.data.data)) {
|
||||||
|
const screenList = response.data.data.map((screen: any) => ({
|
||||||
|
id: screen.screenId,
|
||||||
|
name: screen.screenName,
|
||||||
|
description: screen.description,
|
||||||
|
}));
|
||||||
|
setScreens(screenList);
|
||||||
|
console.log("✅ 화면 목록 설정 완료:", screenList.length, "개");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 화면 목록 로딩 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setScreensLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchScreens();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 검색 필터링 함수
|
||||||
|
const filterScreens = (searchTerm: string) => {
|
||||||
|
if (!searchTerm.trim()) return screens;
|
||||||
|
return screens.filter(
|
||||||
|
(screen) =>
|
||||||
|
screen.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
|
||||||
|
component,
|
||||||
|
config,
|
||||||
|
action: config.action,
|
||||||
|
actionType: config.action?.type,
|
||||||
|
screensCount: screens.length,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -68,8 +131,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="button-action">버튼 액션</Label>
|
<Label htmlFor="button-action">버튼 액션</Label>
|
||||||
<Select
|
<Select
|
||||||
value={config.action || "custom"}
|
value={config.action?.type || "save"}
|
||||||
onValueChange={(value) => onUpdateProperty("componentConfig.action", value)}
|
defaultValue="save"
|
||||||
|
onValueChange={(value) => onUpdateProperty("componentConfig.action", { type: value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="버튼 액션 선택" />
|
<SelectValue placeholder="버튼 액션 선택" />
|
||||||
|
|
@ -84,10 +148,278 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
<SelectItem value="reset">초기화</SelectItem>
|
<SelectItem value="reset">초기화</SelectItem>
|
||||||
<SelectItem value="submit">제출</SelectItem>
|
<SelectItem value="submit">제출</SelectItem>
|
||||||
<SelectItem value="close">닫기</SelectItem>
|
<SelectItem value="close">닫기</SelectItem>
|
||||||
<SelectItem value="custom">사용자 정의</SelectItem>
|
<SelectItem value="modal">모달 열기</SelectItem>
|
||||||
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 열기 액션 설정 */}
|
||||||
|
{config.action?.type === "modal" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">모달 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="modal-title">모달 제목</Label>
|
||||||
|
<Input
|
||||||
|
id="modal-title"
|
||||||
|
placeholder="모달 제목을 입력하세요"
|
||||||
|
value={config.action?.modalTitle || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
modalTitle: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="modal-size">모달 크기</Label>
|
||||||
|
<Select
|
||||||
|
value={config.action?.modalSize || "md"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
modalSize: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="모달 크기 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="sm">작음 (Small)</SelectItem>
|
||||||
|
<SelectItem value="md">보통 (Medium)</SelectItem>
|
||||||
|
<SelectItem value="lg">큼 (Large)</SelectItem>
|
||||||
|
<SelectItem value="xl">매우 큼 (Extra Large)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-screen-modal">대상 화면 선택</Label>
|
||||||
|
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={modalScreenOpen}
|
||||||
|
className="w-full justify-between h-10"
|
||||||
|
disabled={screensLoading}
|
||||||
|
>
|
||||||
|
{config.action?.targetScreenId
|
||||||
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||||
|
"화면을 선택하세요..."
|
||||||
|
: "화면을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: 'var(--radix-popover-trigger-width)' }}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* 검색 입력 */}
|
||||||
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<Input
|
||||||
|
placeholder="화면 검색..."
|
||||||
|
value={modalSearchTerm}
|
||||||
|
onChange={(e) => setModalSearchTerm(e.target.value)}
|
||||||
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 검색 결과 */}
|
||||||
|
<div className="max-h-[200px] overflow-auto">
|
||||||
|
{(() => {
|
||||||
|
const filteredScreens = filterScreens(modalSearchTerm);
|
||||||
|
if (screensLoading) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||||
|
}
|
||||||
|
if (filteredScreens.length === 0) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||||
|
}
|
||||||
|
return filteredScreens.map((screen, index) => (
|
||||||
|
<div
|
||||||
|
key={`modal-screen-${screen.id}-${index}`}
|
||||||
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
targetScreenId: screen.id,
|
||||||
|
});
|
||||||
|
setModalScreenOpen(false);
|
||||||
|
setModalSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.name}</span>
|
||||||
|
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 페이지 이동 액션 설정 */}
|
||||||
|
{config.action?.type === "navigate" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">페이지 이동 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-screen-nav">이동할 화면 선택</Label>
|
||||||
|
<Popover open={navScreenOpen} onOpenChange={setNavScreenOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={navScreenOpen}
|
||||||
|
className="w-full justify-between h-10"
|
||||||
|
disabled={screensLoading}
|
||||||
|
>
|
||||||
|
{config.action?.targetScreenId
|
||||||
|
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
|
||||||
|
"화면을 선택하세요..."
|
||||||
|
: "화면을 선택하세요..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start" style={{ width: 'var(--radix-popover-trigger-width)' }}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* 검색 입력 */}
|
||||||
|
<div className="flex items-center border-b px-3 py-2">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<Input
|
||||||
|
placeholder="화면 검색..."
|
||||||
|
value={navSearchTerm}
|
||||||
|
onChange={(e) => setNavSearchTerm(e.target.value)}
|
||||||
|
className="border-0 p-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 검색 결과 */}
|
||||||
|
<div className="max-h-[200px] overflow-auto">
|
||||||
|
{(() => {
|
||||||
|
const filteredScreens = filterScreens(navSearchTerm);
|
||||||
|
if (screensLoading) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">화면 목록을 불러오는 중...</div>;
|
||||||
|
}
|
||||||
|
if (filteredScreens.length === 0) {
|
||||||
|
return <div className="p-3 text-sm text-gray-500">검색 결과가 없습니다.</div>;
|
||||||
|
}
|
||||||
|
return filteredScreens.map((screen, index) => (
|
||||||
|
<div
|
||||||
|
key={`navigate-screen-${screen.id}-${index}`}
|
||||||
|
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
targetScreenId: screen.id,
|
||||||
|
});
|
||||||
|
setNavScreenOpen(false);
|
||||||
|
setNavSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{screen.name}</span>
|
||||||
|
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
선택한 화면으로 /screens/{"{"}화면ID{"}"} 형태로 이동합니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="target-url">또는 직접 URL 입력 (고급)</Label>
|
||||||
|
<Input
|
||||||
|
id="target-url"
|
||||||
|
placeholder="예: /admin/users 또는 https://example.com"
|
||||||
|
value={config.action?.targetUrl || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
targetUrl: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">URL을 입력하면 화면 선택보다 우선 적용됩니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 확인 메시지 설정 (모든 액션 공통) */}
|
||||||
|
{config.action?.type && config.action.type !== "cancel" && config.action.type !== "close" && (
|
||||||
|
<div className="mt-4 space-y-4 rounded-lg border bg-blue-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">확인 메시지 설정</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="confirm-message">실행 전 확인 메시지</Label>
|
||||||
|
<Input
|
||||||
|
id="confirm-message"
|
||||||
|
placeholder="예: 정말 저장하시겠습니까?"
|
||||||
|
value={config.action?.confirmMessage || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
confirmMessage: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="success-message">성공 메시지</Label>
|
||||||
|
<Input
|
||||||
|
id="success-message"
|
||||||
|
placeholder="예: 저장되었습니다."
|
||||||
|
value={config.action?.successMessage || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
successMessage: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="error-message">오류 메시지</Label>
|
||||||
|
<Input
|
||||||
|
id="error-message"
|
||||||
|
placeholder="예: 저장 중 오류가 발생했습니다."
|
||||||
|
value={config.action?.errorMessage || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdateProperty("componentConfig.action", {
|
||||||
|
...config.action,
|
||||||
|
errorMessage: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,537 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Save,
|
|
||||||
X,
|
|
||||||
Trash2,
|
|
||||||
Edit,
|
|
||||||
Plus,
|
|
||||||
RotateCcw,
|
|
||||||
Send,
|
|
||||||
ExternalLink,
|
|
||||||
MousePointer,
|
|
||||||
Settings,
|
|
||||||
AlertTriangle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { ButtonActionType, ButtonTypeConfig, WidgetComponent, ScreenDefinition } from "@/types/screen";
|
|
||||||
import { screenApi } from "@/lib/api/screen";
|
|
||||||
|
|
||||||
interface ButtonConfigPanelProps {
|
|
||||||
component: WidgetComponent;
|
|
||||||
onUpdateComponent: (updates: Partial<WidgetComponent>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionTypeOptions: { value: ButtonActionType; label: string; icon: React.ReactNode; color: string }[] = [
|
|
||||||
{ value: "save", label: "저장", icon: <Save className="h-4 w-4" />, color: "#3b82f6" },
|
|
||||||
{ value: "delete", label: "삭제", icon: <Trash2 className="h-4 w-4" />, color: "#ef4444" },
|
|
||||||
{ value: "edit", label: "수정", icon: <Edit className="h-4 w-4" />, color: "#f59e0b" },
|
|
||||||
{ value: "add", label: "추가", icon: <Plus className="h-4 w-4" />, color: "#10b981" },
|
|
||||||
{ value: "search", label: "검색", icon: <MousePointer className="h-4 w-4" />, color: "#8b5cf6" },
|
|
||||||
{ value: "reset", label: "초기화", icon: <RotateCcw className="h-4 w-4" />, color: "#6b7280" },
|
|
||||||
{ value: "submit", label: "제출", icon: <Send className="h-4 w-4" />, color: "#059669" },
|
|
||||||
{ value: "close", label: "닫기", icon: <X className="h-4 w-4" />, color: "#6b7280" },
|
|
||||||
{ value: "popup", label: "모달 열기", icon: <ExternalLink className="h-4 w-4" />, color: "#8b5cf6" },
|
|
||||||
{ value: "navigate", label: "페이지 이동", icon: <ExternalLink className="h-4 w-4" />, color: "#0ea5e9" },
|
|
||||||
{ value: "custom", label: "사용자 정의", icon: <Settings className="h-4 w-4" />, color: "#64748b" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateComponent }) => {
|
|
||||||
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
|
||||||
|
|
||||||
// 로컬 상태 관리
|
|
||||||
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>(() => {
|
|
||||||
const defaultConfig = {
|
|
||||||
actionType: "custom" as ButtonActionType,
|
|
||||||
variant: "default" as ButtonVariant,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultConfig,
|
|
||||||
...config, // 저장된 값이 기본값을 덮어씀
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 화면 목록 상태
|
|
||||||
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
|
|
||||||
const [screensLoading, setScreensLoading] = useState(false);
|
|
||||||
|
|
||||||
// 화면 목록 로드 함수
|
|
||||||
const loadScreens = async () => {
|
|
||||||
try {
|
|
||||||
setScreensLoading(true);
|
|
||||||
const response = await screenApi.getScreens({ size: 1000 }); // 모든 화면 가져오기
|
|
||||||
setScreens(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("화면 목록 로드 실패:", error);
|
|
||||||
} finally {
|
|
||||||
setScreensLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 모달 또는 네비게이션 액션 타입일 때 화면 목록 로드
|
|
||||||
useEffect(() => {
|
|
||||||
if (localConfig.actionType === "popup" || localConfig.actionType === "navigate") {
|
|
||||||
loadScreens();
|
|
||||||
}
|
|
||||||
}, [localConfig.actionType]);
|
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
||||||
useEffect(() => {
|
|
||||||
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
|
|
||||||
|
|
||||||
// 기본값 설정 (실제 값이 있으면 덮어쓰지 않음)
|
|
||||||
const defaultConfig = {
|
|
||||||
actionType: "custom" as ButtonActionType,
|
|
||||||
variant: "default" as ButtonVariant,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 실제 저장된 값이 우선순위를 가지도록 설정
|
|
||||||
setLocalConfig({
|
|
||||||
...defaultConfig,
|
|
||||||
...newConfig, // 저장된 값이 기본값을 덮어씀
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("🔄 ButtonConfigPanel 로컬 상태 동기화:", {
|
|
||||||
componentId: component.id,
|
|
||||||
savedConfig: newConfig,
|
|
||||||
finalConfig: { ...defaultConfig, ...newConfig },
|
|
||||||
});
|
|
||||||
}, [component.webTypeConfig, component.id]);
|
|
||||||
|
|
||||||
// 설정 업데이트 함수
|
|
||||||
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
|
|
||||||
const newConfig = { ...localConfig, ...updates };
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
|
|
||||||
// 스타일 업데이트도 함께 적용
|
|
||||||
const styleUpdates: any = {};
|
|
||||||
if (updates.backgroundColor) styleUpdates.backgroundColor = updates.backgroundColor;
|
|
||||||
if (updates.textColor) styleUpdates.color = updates.textColor;
|
|
||||||
if (updates.borderColor) styleUpdates.borderColor = updates.borderColor;
|
|
||||||
|
|
||||||
onUpdateComponent({
|
|
||||||
webTypeConfig: newConfig,
|
|
||||||
...(Object.keys(styleUpdates).length > 0 && {
|
|
||||||
style: { ...component.style, ...styleUpdates },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 액션 타입 변경 시 기본값 설정
|
|
||||||
const handleActionTypeChange = (actionType: ButtonActionType) => {
|
|
||||||
const actionOption = actionTypeOptions.find((opt) => opt.value === actionType);
|
|
||||||
const updates: Partial<ButtonTypeConfig> = { actionType };
|
|
||||||
|
|
||||||
// 액션 타입에 따른 기본 설정
|
|
||||||
switch (actionType) {
|
|
||||||
case "save":
|
|
||||||
updates.variant = "default";
|
|
||||||
updates.backgroundColor = "#3b82f6";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
// 버튼 라벨과 스타일도 업데이트
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "저장",
|
|
||||||
style: { ...component.style, backgroundColor: "#3b82f6", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "close":
|
|
||||||
updates.variant = "outline";
|
|
||||||
updates.backgroundColor = "transparent";
|
|
||||||
updates.textColor = "#6b7280";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "닫기",
|
|
||||||
style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "delete":
|
|
||||||
updates.variant = "destructive";
|
|
||||||
updates.backgroundColor = "#ef4444";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
updates.confirmMessage = "정말로 삭제하시겠습니까?";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "삭제",
|
|
||||||
style: { ...component.style, backgroundColor: "#ef4444", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "edit":
|
|
||||||
updates.backgroundColor = "#f59e0b";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "수정",
|
|
||||||
style: { ...component.style, backgroundColor: "#f59e0b", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "add":
|
|
||||||
updates.backgroundColor = "#10b981";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "추가",
|
|
||||||
style: { ...component.style, backgroundColor: "#10b981", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "search":
|
|
||||||
updates.backgroundColor = "#8b5cf6";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "검색",
|
|
||||||
style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "reset":
|
|
||||||
updates.variant = "outline";
|
|
||||||
updates.backgroundColor = "transparent";
|
|
||||||
updates.textColor = "#6b7280";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "초기화",
|
|
||||||
style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "submit":
|
|
||||||
updates.backgroundColor = "#059669";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "제출",
|
|
||||||
style: { ...component.style, backgroundColor: "#059669", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "popup":
|
|
||||||
updates.backgroundColor = "#8b5cf6";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
updates.popupTitle = "상세 정보";
|
|
||||||
updates.popupContent = "여기에 모달 내용을 입력하세요.";
|
|
||||||
updates.popupSize = "md";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "상세보기",
|
|
||||||
style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "navigate":
|
|
||||||
updates.backgroundColor = "#0ea5e9";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
updates.navigateType = "url";
|
|
||||||
updates.navigateUrl = "/";
|
|
||||||
updates.navigateTarget = "_self";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "이동",
|
|
||||||
style: { ...component.style, backgroundColor: "#0ea5e9", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "custom":
|
|
||||||
updates.backgroundColor = "#64748b";
|
|
||||||
updates.textColor = "#ffffff";
|
|
||||||
onUpdateComponent({
|
|
||||||
label: "버튼",
|
|
||||||
style: { ...component.style, backgroundColor: "#64748b", color: "#ffffff" },
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로컬 상태 업데이트 후 webTypeConfig도 함께 업데이트
|
|
||||||
const newConfig = { ...localConfig, ...updates };
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
|
|
||||||
// webTypeConfig를 마지막에 다시 업데이트하여 확실히 저장되도록 함
|
|
||||||
setTimeout(() => {
|
|
||||||
onUpdateComponent({
|
|
||||||
webTypeConfig: newConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("🎯 ButtonActionType webTypeConfig 최종 업데이트:", {
|
|
||||||
actionType,
|
|
||||||
newConfig,
|
|
||||||
componentId: component.id,
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
버튼 기능 설정
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{/* 액션 타입 선택 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs font-medium">버튼 기능</Label>
|
|
||||||
<Select value={localConfig.actionType} onValueChange={handleActionTypeChange}>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{actionTypeOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{option.icon}
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{selectedActionOption && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
||||||
{selectedActionOption.icon}
|
|
||||||
<span>{selectedActionOption.label}</span>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
style={{ backgroundColor: selectedActionOption.color + "20", color: selectedActionOption.color }}
|
|
||||||
>
|
|
||||||
{selectedActionOption.value}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* 기본 설정 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="text-xs font-medium">기본 설정</Label>
|
|
||||||
|
|
||||||
{/* 버튼 텍스트 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">버튼 텍스트</Label>
|
|
||||||
<Input
|
|
||||||
value={component.label || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newValue = e.target.value;
|
|
||||||
onUpdateComponent({ label: newValue });
|
|
||||||
}}
|
|
||||||
placeholder="버튼에 표시될 텍스트"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 버튼 스타일 */}
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">스타일</Label>
|
|
||||||
<Select value={localConfig.variant} onValueChange={(value) => updateConfig({ variant: value as any })}>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">기본</SelectItem>
|
|
||||||
<SelectItem value="destructive">위험</SelectItem>
|
|
||||||
<SelectItem value="outline">외곽선</SelectItem>
|
|
||||||
<SelectItem value="secondary">보조</SelectItem>
|
|
||||||
<SelectItem value="ghost">투명</SelectItem>
|
|
||||||
<SelectItem value="link">링크</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 아이콘 설정 */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">아이콘 (Lucide 아이콘 이름)</Label>
|
|
||||||
<Input
|
|
||||||
value={localConfig.icon || ""}
|
|
||||||
onChange={(e) => updateConfig({ icon: e.target.value })}
|
|
||||||
placeholder="예: Save, Edit, Trash2"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* 액션별 세부 설정 */}
|
|
||||||
{localConfig.actionType === "delete" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
||||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
|
||||||
삭제 확인 설정
|
|
||||||
</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">확인 메시지</Label>
|
|
||||||
<Input
|
|
||||||
value={localConfig.confirmMessage || ""}
|
|
||||||
onChange={(e) => updateConfig({ confirmMessage: e.target.value })}
|
|
||||||
placeholder="정말로 삭제하시겠습니까?"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{localConfig.actionType === "popup" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
||||||
<ExternalLink className="h-3 w-3 text-purple-500" />
|
|
||||||
모달 설정
|
|
||||||
</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">모달로 열 화면</Label>
|
|
||||||
<Select
|
|
||||||
value={localConfig.popupScreenId?.toString() || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateConfig({
|
|
||||||
popupScreenId: value === "none" ? undefined : parseInt(value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={screensLoading}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue placeholder={screensLoading ? "로딩 중..." : "화면을 선택하세요"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">선택 안함</SelectItem>
|
|
||||||
{screens.map((screen) => (
|
|
||||||
<SelectItem key={screen.screenId} value={screen.screenId.toString()}>
|
|
||||||
{screen.screenName} ({screen.screenCode})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{localConfig.popupScreenId && <p className="text-xs text-gray-500">선택된 화면이 모달로 열립니다</p>}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">모달 제목</Label>
|
|
||||||
<Input
|
|
||||||
value={localConfig.popupTitle || ""}
|
|
||||||
onChange={(e) => updateConfig({ popupTitle: e.target.value })}
|
|
||||||
placeholder="상세 정보"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!localConfig.popupScreenId && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">모달 내용</Label>
|
|
||||||
<Textarea
|
|
||||||
value={localConfig.popupContent || ""}
|
|
||||||
onChange={(e) => updateConfig({ popupContent: e.target.value })}
|
|
||||||
placeholder="여기에 모달 내용을 입력하세요."
|
|
||||||
className="h-16 resize-none text-xs"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500">화면을 선택하지 않으면 이 내용이 모달에 표시됩니다</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{localConfig.actionType === "navigate" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
||||||
<ExternalLink className="h-3 w-3 text-blue-500" />
|
|
||||||
페이지 이동 설정
|
|
||||||
</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">이동 방식</Label>
|
|
||||||
<Select
|
|
||||||
value={localConfig.navigateType || "url"}
|
|
||||||
onValueChange={(value) => updateConfig({ navigateType: value as "url" | "screen" })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="url">URL 직접 입력</SelectItem>
|
|
||||||
<SelectItem value="screen">화면 선택</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(localConfig.navigateType || "url") === "url" ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">이동할 URL</Label>
|
|
||||||
<Input
|
|
||||||
value={localConfig.navigateUrl || ""}
|
|
||||||
onChange={(e) => updateConfig({ navigateUrl: e.target.value })}
|
|
||||||
placeholder="/admin/users"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">이동할 화면</Label>
|
|
||||||
<Select
|
|
||||||
value={localConfig.navigateScreenId?.toString() || ""}
|
|
||||||
onValueChange={(value) => updateConfig({ navigateScreenId: value ? parseInt(value) : undefined })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue placeholder="화면을 선택하세요" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{screensLoading ? (
|
|
||||||
<SelectItem value="" disabled>
|
|
||||||
화면 목록 로딩중...
|
|
||||||
</SelectItem>
|
|
||||||
) : screens.length === 0 ? (
|
|
||||||
<SelectItem value="" disabled>
|
|
||||||
사용 가능한 화면이 없습니다
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
screens.map((screen) => (
|
|
||||||
<SelectItem key={screen.screenId} value={screen.screenId.toString()}>
|
|
||||||
{screen.screenName} ({screen.screenCode})
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">열기 방식</Label>
|
|
||||||
<Select
|
|
||||||
value={localConfig.navigateTarget || "_self"}
|
|
||||||
onValueChange={(value) => updateConfig({ navigateTarget: value as "_self" | "_blank" })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="_self">현재 창</SelectItem>
|
|
||||||
<SelectItem value="_blank">새 창</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{localConfig.actionType === "custom" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
||||||
<Settings className="h-3 w-3 text-gray-500" />
|
|
||||||
사용자 정의 액션
|
|
||||||
</Label>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">JavaScript 코드</Label>
|
|
||||||
<Textarea
|
|
||||||
value={localConfig.customAction || ""}
|
|
||||||
onChange={(e) => updateConfig({ customAction: e.target.value })}
|
|
||||||
placeholder="alert('버튼이 클릭되었습니다!');"
|
|
||||||
className="h-16 resize-none font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
JavaScript 코드를 입력하세요. 예: alert(), console.log(), 함수 호출 등
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableInfo,
|
TableInfo,
|
||||||
LayoutComponent,
|
LayoutComponent,
|
||||||
} from "@/types/screen";
|
} from "@/types/screen";
|
||||||
import { ButtonConfigPanel } from "./ButtonConfigPanel";
|
// 레거시 ButtonConfigPanel 제거됨
|
||||||
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
||||||
|
|
||||||
// 새로운 컴포넌트 설정 패널들 import
|
// 새로운 컴포넌트 설정 패널들 import
|
||||||
|
|
@ -26,6 +26,9 @@ import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
|
||||||
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
|
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
|
||||||
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
|
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
|
||||||
|
|
||||||
|
// 동적 컴포넌트 설정 패널
|
||||||
|
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
||||||
|
|
||||||
interface DetailSettingsPanelProps {
|
interface DetailSettingsPanelProps {
|
||||||
selectedComponent?: ComponentData;
|
selectedComponent?: ComponentData;
|
||||||
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
||||||
|
|
@ -878,13 +881,18 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
return renderLayoutConfig(selectedComponent as LayoutComponent);
|
return renderLayoutConfig(selectedComponent as LayoutComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedComponent.type !== "widget" && selectedComponent.type !== "file" && selectedComponent.type !== "button") {
|
if (
|
||||||
|
selectedComponent.type !== "widget" &&
|
||||||
|
selectedComponent.type !== "file" &&
|
||||||
|
selectedComponent.type !== "button" &&
|
||||||
|
selectedComponent.type !== "component"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
||||||
<Settings className="mb-4 h-12 w-12 text-gray-400" />
|
<Settings className="mb-4 h-12 w-12 text-gray-400" />
|
||||||
<h3 className="mb-2 text-lg font-medium text-gray-900">설정할 수 없는 컴포넌트입니다</h3>
|
<h3 className="mb-2 text-lg font-medium text-gray-900">설정할 수 없는 컴포넌트입니다</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
상세 설정은 위젯, 파일, 버튼, 레이아웃 컴포넌트에서만 사용할 수 있습니다.
|
상세 설정은 위젯, 파일, 버튼, 컴포넌트, 레이아웃에서만 사용할 수 있습니다.
|
||||||
<br />
|
<br />
|
||||||
현재 선택된 컴포넌트: {selectedComponent.type}
|
현재 선택된 컴포넌트: {selectedComponent.type}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -924,9 +932,45 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 버튼 컴포넌트인 경우 ButtonConfigPanel 렌더링
|
// 레거시 버튼을 새로운 컴포넌트 시스템으로 강제 변환
|
||||||
if (selectedComponent.type === "button") {
|
if (selectedComponent.type === "button") {
|
||||||
const buttonWidget = selectedComponent as WidgetComponent;
|
console.log("🔄 레거시 버튼을 새로운 컴포넌트 시스템으로 변환:", selectedComponent);
|
||||||
|
|
||||||
|
// 레거시 버튼을 새로운 시스템으로 변환
|
||||||
|
const convertedComponent = {
|
||||||
|
...selectedComponent,
|
||||||
|
type: "component" as const,
|
||||||
|
componentConfig: {
|
||||||
|
type: "button-primary",
|
||||||
|
webType: "button",
|
||||||
|
...selectedComponent.componentConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 변환된 컴포넌트로 DB 업데이트
|
||||||
|
onUpdateProperty(selectedComponent.id, "type", "component");
|
||||||
|
onUpdateProperty(selectedComponent.id, "componentConfig", convertedComponent.componentConfig);
|
||||||
|
|
||||||
|
// 변환된 컴포넌트로 처리 계속
|
||||||
|
selectedComponent = convertedComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로운 컴포넌트 시스템 처리 (type: "component")
|
||||||
|
if (selectedComponent.type === "component") {
|
||||||
|
const componentId = selectedComponent.componentConfig?.type;
|
||||||
|
const webType = selectedComponent.componentConfig?.webType;
|
||||||
|
|
||||||
|
console.log("🔧 새로운 컴포넌트 시스템 설정 패널:", { componentId, webType });
|
||||||
|
|
||||||
|
if (!componentId) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
||||||
|
<Settings className="mb-4 h-12 w-12 text-gray-400" />
|
||||||
|
<h3 className="mb-2 text-lg font-medium text-gray-900">컴포넌트 ID가 없습니다</h3>
|
||||||
|
<p className="text-sm text-gray-500">componentConfig.type이 설정되지 않았습니다.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
|
|
@ -934,22 +978,33 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b border-gray-200 p-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Settings className="h-4 w-4 text-gray-600" />
|
<Settings className="h-4 w-4 text-gray-600" />
|
||||||
<h3 className="font-medium text-gray-900">버튼 상세 설정</h3>
|
<h3 className="font-medium text-gray-900">컴포넌트 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center space-x-2">
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
<span className="text-sm text-gray-600">타입:</span>
|
<span className="text-sm text-gray-600">컴포넌트:</span>
|
||||||
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">버튼</span>
|
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">라벨: {buttonWidget.label || "버튼"}</div>
|
{webType && (
|
||||||
|
<div className="mt-1 flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-gray-600">웹타입:</span>
|
||||||
|
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">{webType}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedComponent.columnName && (
|
||||||
|
<div className="mt-1 text-xs text-gray-500">컬럼: {selectedComponent.columnName}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 버튼 설정 영역 */}
|
{/* 컴포넌트 설정 패널 */}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<ButtonConfigPanel
|
<DynamicComponentConfigPanel
|
||||||
component={buttonWidget}
|
componentId={componentId}
|
||||||
onUpdateComponent={(updates) => {
|
config={selectedComponent.componentConfig || {}}
|
||||||
Object.entries(updates).forEach(([key, value]) => {
|
onChange={(newConfig) => {
|
||||||
onUpdateProperty(buttonWidget.id, key, value);
|
console.log("🔧 컴포넌트 설정 변경:", newConfig);
|
||||||
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
|
onUpdateProperty(selectedComponent.id, `componentConfig.${key}`, value);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -958,6 +1013,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 기존 위젯 시스템 처리 (type: "widget")
|
||||||
const widget = selectedComponent as WidgetComponent;
|
const widget = selectedComponent as WidgetComponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -535,7 +535,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, positionX: newValue }));
|
setLocalInputs((prev) => ({ ...prev, positionX: newValue }));
|
||||||
onUpdateProperty("position", { ...selectedComponent.position, x: Number(newValue) });
|
onUpdateProperty("position.x", Number(newValue));
|
||||||
}}
|
}}
|
||||||
className={`mt-1 ${
|
className={`mt-1 ${
|
||||||
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
|
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
|
||||||
|
|
@ -565,7 +565,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, positionY: newValue }));
|
setLocalInputs((prev) => ({ ...prev, positionY: newValue }));
|
||||||
onUpdateProperty("position", { ...selectedComponent.position, y: Number(newValue) });
|
onUpdateProperty("position.y", Number(newValue));
|
||||||
}}
|
}}
|
||||||
className={`mt-1 ${
|
className={`mt-1 ${
|
||||||
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
|
dragState?.isDragging && dragState.draggedComponent?.id === selectedComponent?.id
|
||||||
|
|
@ -590,7 +590,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, width: newValue }));
|
setLocalInputs((prev) => ({ ...prev, width: newValue }));
|
||||||
onUpdateProperty("size", { ...(selectedComponent.size || {}), width: Number(newValue) });
|
onUpdateProperty("size.width", Number(newValue));
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
|
|
@ -607,7 +607,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, height: newValue }));
|
setLocalInputs((prev) => ({ ...prev, height: newValue }));
|
||||||
onUpdateProperty("size", { ...(selectedComponent.size || {}), height: Number(newValue) });
|
onUpdateProperty("size.height", Number(newValue));
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
|
|
@ -633,7 +633,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
setLocalInputs((prev) => ({ ...prev, positionZ: newValue }));
|
setLocalInputs((prev) => ({ ...prev, positionZ: newValue }));
|
||||||
onUpdateProperty("position", { ...selectedComponent.position, z: Number(newValue) });
|
onUpdateProperty("position.z", Number(newValue));
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
|
|
@ -406,8 +406,19 @@ export class AutoRegisteringComponentRenderer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComponentRegistry의 검증 로직 재사용
|
// 기본적인 검증만 수행
|
||||||
return ComponentRegistry["validateComponentDefinition"](definition);
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
if (!definition.id) errors.push("id가 필요합니다");
|
||||||
|
if (!definition.name) errors.push("name이 필요합니다");
|
||||||
|
if (!definition.category) errors.push("category가 필요합니다");
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ export interface ComponentRenderer {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
|
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void;
|
||||||
onZoneClick?: (zoneId: string) => void;
|
onZoneClick?: (zoneId: string) => void;
|
||||||
|
// 버튼 액션을 위한 추가 props
|
||||||
|
screenId?: number;
|
||||||
|
tableName?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}): React.ReactElement;
|
}): React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +74,11 @@ export interface DynamicComponentRendererProps {
|
||||||
onDragStart?: (e: React.DragEvent) => void;
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
// 버튼 액션을 위한 추가 props
|
||||||
|
screenId?: number;
|
||||||
|
tableName?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +94,16 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// component_config에서 실제 컴포넌트 타입 추출
|
// component_config에서 실제 컴포넌트 타입 추출
|
||||||
const componentType = component.componentConfig?.type || component.type;
|
const componentType = component.componentConfig?.type || component.type;
|
||||||
|
|
||||||
|
console.log("🔍 컴포넌트 타입 추출:", {
|
||||||
|
componentId: component.id,
|
||||||
|
componentConfigType: component.componentConfig?.type,
|
||||||
|
componentType: component.type,
|
||||||
|
finalComponentType: componentType,
|
||||||
|
componentConfig: component.componentConfig,
|
||||||
|
propsScreenId: props.screenId,
|
||||||
|
propsTableName: props.tableName,
|
||||||
|
});
|
||||||
|
|
||||||
// 레이아웃 컴포넌트 처리
|
// 레이아웃 컴포넌트 처리
|
||||||
if (componentType === "layout") {
|
if (componentType === "layout") {
|
||||||
return (
|
return (
|
||||||
|
|
@ -106,14 +126,22 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
componentType,
|
componentType,
|
||||||
componentConfig: component.componentConfig,
|
componentConfig: component.componentConfig,
|
||||||
registeredTypes: legacyComponentRegistry.getRegisteredTypes(),
|
newSystemRegistered: ComponentRegistry.getAllComponents().map((c) => c.id),
|
||||||
hasRenderer: legacyComponentRegistry.has(componentType),
|
legacySystemRegistered: legacyComponentRegistry.getRegisteredTypes(),
|
||||||
actualRenderer: legacyComponentRegistry.get(componentType),
|
hasLegacyRenderer: legacyComponentRegistry.has(componentType),
|
||||||
mapSize: legacyComponentRegistry.getRegisteredTypes().length,
|
actualLegacyRenderer: legacyComponentRegistry.get(componentType),
|
||||||
|
legacyMapSize: legacyComponentRegistry.getRegisteredTypes().length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. 새 컴포넌트 시스템에서 먼저 조회
|
// 1. 새 컴포넌트 시스템에서 먼저 조회
|
||||||
const newComponent = ComponentRegistry.getComponent(componentType);
|
const newComponent = ComponentRegistry.getComponent(componentType);
|
||||||
|
console.log("🔍 새 컴포넌트 시스템 조회:", {
|
||||||
|
componentType,
|
||||||
|
found: !!newComponent,
|
||||||
|
component: newComponent,
|
||||||
|
registeredTypes: ComponentRegistry.getAllComponents().map((c) => c.id),
|
||||||
|
});
|
||||||
|
|
||||||
if (newComponent) {
|
if (newComponent) {
|
||||||
console.log("✨ 새 컴포넌트 시스템에서 발견:", componentType);
|
console.log("✨ 새 컴포넌트 시스템에서 발견:", componentType);
|
||||||
|
|
||||||
|
|
@ -121,18 +149,46 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
try {
|
try {
|
||||||
const NewComponentRenderer = newComponent.component;
|
const NewComponentRenderer = newComponent.component;
|
||||||
if (NewComponentRenderer) {
|
if (NewComponentRenderer) {
|
||||||
|
console.log("🔧 컴포넌트 렌더링 props:", {
|
||||||
|
componentType,
|
||||||
|
componentId: component.id,
|
||||||
|
screenId: props.screenId,
|
||||||
|
tableName: props.tableName,
|
||||||
|
onRefresh: !!props.onRefresh,
|
||||||
|
onClose: !!props.onClose,
|
||||||
|
});
|
||||||
|
// React 전용 props 필터링
|
||||||
|
const {
|
||||||
|
isInteractive,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
|
selectedScreen,
|
||||||
|
onZoneComponentDrop,
|
||||||
|
onZoneClick,
|
||||||
|
componentConfig,
|
||||||
|
...safeProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewComponentRenderer
|
<NewComponentRenderer
|
||||||
{...props}
|
{...safeProps}
|
||||||
component={component}
|
component={component}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
isInteractive={isInteractive}
|
||||||
|
formData={formData}
|
||||||
|
onFormDataChange={onFormDataChange}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
size={component.size || newComponent.defaultSize}
|
size={component.size || newComponent.defaultSize}
|
||||||
position={component.position}
|
position={component.position}
|
||||||
style={component.style}
|
style={component.style}
|
||||||
|
config={component.componentConfig}
|
||||||
componentConfig={component.componentConfig}
|
componentConfig={component.componentConfig}
|
||||||
|
screenId={props.screenId}
|
||||||
|
tableName={props.tableName}
|
||||||
|
onRefresh={props.onRefresh}
|
||||||
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +201,13 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
const renderer = legacyComponentRegistry.get(componentType);
|
const renderer = legacyComponentRegistry.get(componentType);
|
||||||
|
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
console.warn(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`);
|
console.error(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`, {
|
||||||
|
component: component,
|
||||||
|
componentType: componentType,
|
||||||
|
componentConfig: component.componentConfig,
|
||||||
|
availableNewComponents: ComponentRegistry.getAllComponents().map((c) => c.id),
|
||||||
|
availableLegacyComponents: legacyComponentRegistry.getRegisteredTypes(),
|
||||||
|
});
|
||||||
|
|
||||||
// 폴백 렌더링 - 기본 플레이스홀더
|
// 폴백 렌더링 - 기본 플레이스홀더
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,8 @@ const ButtonRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 레지스트리에 등록 - 모든 버튼 타입들
|
// 레지스트리에 등록 - 기본 버튼 타입만 (button-primary는 새 컴포넌트 시스템 사용)
|
||||||
componentRegistry.register("button", ButtonRenderer);
|
componentRegistry.register("button", ButtonRenderer);
|
||||||
componentRegistry.register("button-primary", ButtonRenderer);
|
|
||||||
componentRegistry.register("button-secondary", ButtonRenderer);
|
componentRegistry.register("button-secondary", ButtonRenderer);
|
||||||
|
|
||||||
export { ButtonRenderer };
|
export { ButtonRenderer };
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,33 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
import { ComponentRendererProps } from "@/types/component";
|
||||||
import { ButtonPrimaryConfig } from "./types";
|
import { ButtonPrimaryConfig } from "./types";
|
||||||
|
import {
|
||||||
|
ButtonActionExecutor,
|
||||||
|
ButtonActionContext,
|
||||||
|
ButtonActionType,
|
||||||
|
DEFAULT_BUTTON_ACTIONS,
|
||||||
|
} from "@/lib/utils/buttonActions";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
config?: ButtonPrimaryConfig;
|
config?: ButtonPrimaryConfig;
|
||||||
|
// 추가 props
|
||||||
|
screenId?: number;
|
||||||
|
tableName?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,20 +38,54 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
onRefresh,
|
||||||
|
onClose,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
// 확인 다이얼로그 상태
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
const [pendingAction, setPendingAction] = useState<{
|
||||||
|
type: ButtonActionType;
|
||||||
|
config: any;
|
||||||
|
context: ButtonActionContext;
|
||||||
|
} | null>(null);
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
const componentConfig = {
|
const componentConfig = {
|
||||||
...config,
|
...config,
|
||||||
...component.config,
|
...component.config,
|
||||||
} as ButtonPrimaryConfig;
|
} as ButtonPrimaryConfig;
|
||||||
|
|
||||||
|
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
||||||
|
const processedConfig = { ...componentConfig };
|
||||||
|
if (componentConfig.action && typeof componentConfig.action === "string") {
|
||||||
|
const actionType = componentConfig.action as ButtonActionType;
|
||||||
|
processedConfig.action = {
|
||||||
|
...DEFAULT_BUTTON_ACTIONS[actionType],
|
||||||
|
type: actionType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔧 버튼 컴포넌트 설정:", {
|
||||||
|
originalConfig: componentConfig,
|
||||||
|
processedConfig,
|
||||||
|
component: component,
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
onRefresh,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
|
|
||||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||||
const componentStyle: React.CSSProperties = {
|
const componentStyle: React.CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -44,10 +100,131 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 확인 다이얼로그가 필요한 액션 타입들
|
||||||
|
const confirmationRequiredActions: ButtonActionType[] = ["save", "submit", "delete"];
|
||||||
|
|
||||||
|
// 실제 액션 실행 함수
|
||||||
|
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
|
||||||
|
console.log("🚀 executeAction 시작:", { actionConfig, context });
|
||||||
|
let loadingToast: string | number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("📱 로딩 토스트 표시 시작");
|
||||||
|
// 로딩 토스트 표시
|
||||||
|
loadingToast = toast.loading(
|
||||||
|
actionConfig.type === "save"
|
||||||
|
? "저장 중..."
|
||||||
|
: actionConfig.type === "delete"
|
||||||
|
? "삭제 중..."
|
||||||
|
: actionConfig.type === "submit"
|
||||||
|
? "제출 중..."
|
||||||
|
: "처리 중...",
|
||||||
|
);
|
||||||
|
console.log("📱 로딩 토스트 ID:", loadingToast);
|
||||||
|
|
||||||
|
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
||||||
|
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
||||||
|
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
||||||
|
|
||||||
|
// 로딩 토스트 제거
|
||||||
|
console.log("📱 로딩 토스트 제거");
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
|
|
||||||
|
// 성공 시 토스트 표시
|
||||||
|
const successMessage =
|
||||||
|
actionConfig.successMessage ||
|
||||||
|
(actionConfig.type === "save"
|
||||||
|
? "저장되었습니다."
|
||||||
|
: actionConfig.type === "delete"
|
||||||
|
? "삭제되었습니다."
|
||||||
|
: actionConfig.type === "submit"
|
||||||
|
? "제출되었습니다."
|
||||||
|
: "완료되었습니다.");
|
||||||
|
|
||||||
|
console.log("🎉 성공 토스트 표시:", successMessage);
|
||||||
|
toast.success(successMessage);
|
||||||
|
|
||||||
|
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ executeAction catch 블록 진입:", error);
|
||||||
|
|
||||||
|
// 로딩 토스트 제거
|
||||||
|
if (loadingToast) {
|
||||||
|
console.log("📱 오류 시 로딩 토스트 제거");
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("❌ 버튼 액션 실행 오류:", error);
|
||||||
|
|
||||||
|
// 오류 토스트 표시
|
||||||
|
const errorMessage =
|
||||||
|
actionConfig.errorMessage ||
|
||||||
|
(actionConfig.type === "save"
|
||||||
|
? "저장 중 오류가 발생했습니다."
|
||||||
|
: actionConfig.type === "delete"
|
||||||
|
? "삭제 중 오류가 발생했습니다."
|
||||||
|
: actionConfig.type === "submit"
|
||||||
|
? "제출 중 오류가 발생했습니다."
|
||||||
|
: "처리 중 오류가 발생했습니다.");
|
||||||
|
|
||||||
|
console.log("💥 오류 토스트 표시:", errorMessage);
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 이벤트 핸들러
|
// 이벤트 핸들러
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClick?.();
|
|
||||||
|
// 디자인 모드에서는 기본 onClick만 실행
|
||||||
|
if (isDesignMode) {
|
||||||
|
onClick?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인터랙티브 모드에서 액션 실행
|
||||||
|
if (isInteractive && processedConfig.action) {
|
||||||
|
const context: ButtonActionContext = {
|
||||||
|
formData: formData || {},
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
onFormDataChange,
|
||||||
|
onRefresh,
|
||||||
|
onClose,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 확인이 필요한 액션인지 확인
|
||||||
|
if (confirmationRequiredActions.includes(processedConfig.action.type)) {
|
||||||
|
// 확인 다이얼로그 표시
|
||||||
|
setPendingAction({
|
||||||
|
type: processedConfig.action.type,
|
||||||
|
config: processedConfig.action,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
setShowConfirmDialog(true);
|
||||||
|
} else {
|
||||||
|
// 확인이 필요하지 않은 액션은 바로 실행
|
||||||
|
await executeAction(processedConfig.action, context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 액션이 설정되지 않은 경우 기본 onClick 실행
|
||||||
|
onClick?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 확인 다이얼로그에서 확인 버튼 클릭 시
|
||||||
|
const handleConfirmAction = async () => {
|
||||||
|
if (pendingAction) {
|
||||||
|
await executeAction(pendingAction.config, pendingAction.context);
|
||||||
|
}
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
setPendingAction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 확인 다이얼로그에서 취소 버튼 클릭 시
|
||||||
|
const handleCancelAction = () => {
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
setPendingAction(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM에 전달하면 안 되는 React-specific props 필터링
|
// DOM에 전달하면 안 되는 React-specific props 필터링
|
||||||
|
|
@ -64,33 +241,97 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// 다이얼로그 메시지 생성
|
||||||
|
const getConfirmMessage = () => {
|
||||||
|
if (!pendingAction) return "";
|
||||||
|
|
||||||
|
const customMessage = pendingAction.config.confirmMessage;
|
||||||
|
if (customMessage) return customMessage;
|
||||||
|
|
||||||
|
switch (pendingAction.type) {
|
||||||
|
case "save":
|
||||||
|
return "변경사항을 저장하시겠습니까?";
|
||||||
|
case "delete":
|
||||||
|
return "정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.";
|
||||||
|
case "submit":
|
||||||
|
return "제출하시겠습니까?";
|
||||||
|
default:
|
||||||
|
return "이 작업을 실행하시겠습니까?";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfirmTitle = () => {
|
||||||
|
if (!pendingAction) return "";
|
||||||
|
|
||||||
|
switch (pendingAction.type) {
|
||||||
|
case "save":
|
||||||
|
return "저장 확인";
|
||||||
|
case "delete":
|
||||||
|
return "삭제 확인";
|
||||||
|
case "submit":
|
||||||
|
return "제출 확인";
|
||||||
|
default:
|
||||||
|
return "작업 확인";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={componentStyle} className={className} {...domProps}>
|
<>
|
||||||
<button
|
<div style={componentStyle} className={className} {...domProps}>
|
||||||
type={componentConfig.actionType || "button"}
|
<button
|
||||||
disabled={componentConfig.disabled || false}
|
type={componentConfig.actionType || "button"}
|
||||||
style={{
|
disabled={componentConfig.disabled || false}
|
||||||
width: "100%",
|
style={{
|
||||||
height: "100%",
|
width: "100%",
|
||||||
border: "1px solid #3b82f6",
|
height: "100%",
|
||||||
borderRadius: "4px",
|
border: "1px solid #3b82f6",
|
||||||
backgroundColor: "#3b82f6",
|
borderRadius: "4px",
|
||||||
color: "white",
|
backgroundColor: "#3b82f6",
|
||||||
fontSize: "14px",
|
color: "white",
|
||||||
fontWeight: "500",
|
fontSize: "14px",
|
||||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
fontWeight: "500",
|
||||||
outline: "none",
|
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||||
}}
|
outline: "none",
|
||||||
onClick={handleClick}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
onDragStart={onDragStart}
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
onDragEnd={onDragEnd}
|
}}
|
||||||
>
|
onClick={handleClick}
|
||||||
{componentConfig.text || component.label || "버튼"}
|
onDragStart={onDragStart}
|
||||||
</button>
|
onDragEnd={onDragEnd}
|
||||||
</div>
|
>
|
||||||
|
{processedConfig.text || component.label || "버튼"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 확인 다이얼로그 */}
|
||||||
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{getConfirmTitle()}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>{getConfirmMessage()}</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={handleCancelAction}>취소</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleConfirmAction}>
|
||||||
|
{pendingAction?.type === "save"
|
||||||
|
? "저장"
|
||||||
|
: pendingAction?.type === "delete"
|
||||||
|
? "삭제"
|
||||||
|
: pendingAction?.type === "submit"
|
||||||
|
? "제출"
|
||||||
|
: "확인"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,36 +16,24 @@ export interface ButtonPrimaryConfigPanelProps {
|
||||||
* ButtonPrimary 설정 패널
|
* ButtonPrimary 설정 패널
|
||||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||||
*/
|
*/
|
||||||
export const ButtonPrimaryConfigPanel: React.FC<ButtonPrimaryConfigPanelProps> = ({
|
export const ButtonPrimaryConfigPanel: React.FC<ButtonPrimaryConfigPanelProps> = ({ config, onChange }) => {
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
const handleChange = (key: keyof ButtonPrimaryConfig, value: any) => {
|
const handleChange = (key: keyof ButtonPrimaryConfig, value: any) => {
|
||||||
onChange({ [key]: value });
|
onChange({ [key]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">button-primary 설정</div>
|
||||||
button-primary 설정
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 버튼 관련 설정 */}
|
{/* 버튼 관련 설정 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="text">버튼 텍스트</Label>
|
<Label htmlFor="text">버튼 텍스트</Label>
|
||||||
<Input
|
<Input id="text" value={config.text || ""} onChange={(e) => handleChange("text", e.target.value)} />
|
||||||
id="text"
|
|
||||||
value={config.text || ""}
|
|
||||||
onChange={(e) => handleChange("text", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="actionType">액션 타입</Label>
|
<Label htmlFor="actionType">액션 타입</Label>
|
||||||
<Select
|
<Select value={config.actionType || "button"} onValueChange={(value) => handleChange("actionType", value)}>
|
||||||
value={config.actionType || "button"}
|
|
||||||
onValueChange={(value) => handleChange("actionType", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,14 @@ export const ButtonPrimaryDefinition = createComponentDefinition({
|
||||||
webType: "button",
|
webType: "button",
|
||||||
component: ButtonPrimaryWrapper,
|
component: ButtonPrimaryWrapper,
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
text: "버튼",
|
text: "저장",
|
||||||
actionType: "button",
|
actionType: "button",
|
||||||
variant: "primary",
|
variant: "primary",
|
||||||
|
action: {
|
||||||
|
type: "save",
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultSize: { width: 120, height: 36 },
|
defaultSize: { width: 120, height: 36 },
|
||||||
configPanel: ButtonPrimaryConfigPanel,
|
configPanel: ButtonPrimaryConfigPanel,
|
||||||
|
|
@ -34,11 +39,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({
|
||||||
documentation: "https://docs.example.com/components/button-primary",
|
documentation: "https://docs.example.com/components/button-primary",
|
||||||
});
|
});
|
||||||
|
|
||||||
// ComponentRegistry에 등록
|
// 컴포넌트는 ButtonPrimaryRenderer에서 자동 등록됩니다
|
||||||
import { ComponentRegistry } from "../../ComponentRegistry";
|
|
||||||
ComponentRegistry.registerComponent(ButtonPrimaryDefinition);
|
|
||||||
|
|
||||||
console.log("🚀 ButtonPrimary 컴포넌트 등록 완료");
|
|
||||||
|
|
||||||
// 타입 내보내기
|
// 타입 내보내기
|
||||||
export type { ButtonPrimaryConfig } from "./types";
|
export type { ButtonPrimaryConfig } from "./types";
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ComponentConfig } from "@/types/component";
|
import { ComponentConfig } from "@/types/component";
|
||||||
|
import { ButtonActionConfig } from "@/lib/utils/buttonActions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ButtonPrimary 컴포넌트 설정 타입
|
* ButtonPrimary 컴포넌트 설정 타입
|
||||||
*/
|
*/
|
||||||
export interface ButtonPrimaryConfig extends ComponentConfig {
|
export interface ButtonPrimaryConfig extends ComponentConfig {
|
||||||
// 버튼 관련 설정
|
// 버튼 관련 설정
|
||||||
text?: string;
|
text?: string;
|
||||||
actionType?: "button" | "submit" | "reset";
|
actionType?: "button" | "submit" | "reset";
|
||||||
variant?: "primary" | "secondary" | "danger";
|
variant?: "primary" | "secondary" | "danger";
|
||||||
|
|
||||||
|
// 버튼 액션 설정
|
||||||
|
action?: ButtonActionConfig;
|
||||||
|
|
||||||
// 공통 설정
|
// 공통 설정
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
|
|
||||||
// 스타일 관련
|
// 스타일 관련
|
||||||
variant?: "default" | "outlined" | "filled";
|
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
|
|
||||||
// 이벤트 관련
|
// 이벤트 관련
|
||||||
onChange?: (value: any) => void;
|
onChange?: (value: any) => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
|
|
@ -39,7 +42,7 @@ export interface ButtonPrimaryProps {
|
||||||
config?: ButtonPrimaryConfig;
|
config?: ButtonPrimaryConfig;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
// 이벤트 핸들러
|
// 이벤트 핸들러
|
||||||
onChange?: (value: any) => void;
|
onChange?: (value: any) => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,23 +86,33 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && (
|
||||||
|
<span style={{
|
||||||
|
color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label
|
<label
|
||||||
style={{
|
style={{display: "flex",
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
@ -105,18 +122,20 @@ export const CheckboxBasicComponent: React.FC<CheckboxBasicComponentProps> = ({
|
||||||
checked={component.value === true || component.value === "true"}
|
checked={component.value === true || component.value === "true"}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
style={{
|
style={{width: "16px",
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
height: "16px",
|
||||||
accentColor: "#3b82f6",
|
accentColor: "#3b82f6",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (component.onChange) {
|
if (component.onChange) {
|
||||||
component.onChange(e.target.checked);
|
component.onChange(e.target.checked);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "#374151" }}>{componentConfig.checkboxLabel || component.text || "체크박스"}</span>
|
<span style={{color: "#374151",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),}}>{componentConfig.checkboxLabel || component.text || "체크박스"}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,10 +86,20 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && (
|
||||||
|
<span style={{
|
||||||
|
color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -93,15 +110,15 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
readOnly={componentConfig.readonly || false}
|
readOnly={componentConfig.readonly || false}
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
border: "1px solid #d1d5db",
|
border: "1px solid #d1d5db",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,10 @@ export const DividerLineComponent: React.FC<DividerLineComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,16 +86,20 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
border: "2px dashed #d1d5db",
|
border: "2px dashed #d1d5db",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
|
|
@ -99,7 +110,9 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
backgroundColor: "#f9fafb",
|
backgroundColor: "#f9fafb",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
@ -116,6 +129,8 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (component.onChange) {
|
if (component.onChange) {
|
||||||
|
|
@ -124,10 +139,22 @@ export const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ textAlign: "center", color: "#6b7280", fontSize: "14px" }}>
|
<div style={{textAlign: "center", color: "#6b7280", fontSize: "14px",
|
||||||
<div style={{ fontSize: "24px", marginBottom: "8px" }}>📁</div>
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
<div style={{ fontWeight: "500" }}>파일을 선택하거나 드래그하세요</div>
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
<div style={{ fontSize: "12px", marginTop: "4px" }}>
|
}}>
|
||||||
|
<div style={{fontSize: "24px", marginBottom: "8px",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>📁</div>
|
||||||
|
<div style={{fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>파일을 선택하거나 드래그하세요</div>
|
||||||
|
<div style={{fontSize: "12px", marginTop: "4px",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
{componentConfig.accept && `지원 형식: ${componentConfig.accept}`}
|
{componentConfig.accept && `지원 형식: ${componentConfig.accept}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,10 @@ export const ImageDisplayComponent: React.FC<ImageDisplayComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ import "./select-basic/SelectBasicRenderer";
|
||||||
import "./checkbox-basic/CheckboxBasicRenderer";
|
import "./checkbox-basic/CheckboxBasicRenderer";
|
||||||
import "./radio-basic/RadioBasicRenderer";
|
import "./radio-basic/RadioBasicRenderer";
|
||||||
import "./date-input/DateInputRenderer";
|
import "./date-input/DateInputRenderer";
|
||||||
import "./label-basic/LabelBasicRenderer";
|
// import "./label-basic/LabelBasicRenderer"; // 제거됨 - text-display로 교체
|
||||||
|
import "./text-display/TextDisplayRenderer";
|
||||||
import "./file-upload/FileUploadRenderer";
|
import "./file-upload/FileUploadRenderer";
|
||||||
import "./slider-basic/SliderBasicRenderer";
|
import "./slider-basic/SliderBasicRenderer";
|
||||||
import "./toggle-switch/ToggleSwitchRenderer";
|
import "./toggle-switch/ToggleSwitchRenderer";
|
||||||
|
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
|
||||||
import { LabelBasicConfig } from "./types";
|
|
||||||
|
|
||||||
export interface LabelBasicComponentProps extends ComponentRendererProps {
|
|
||||||
config?: LabelBasicConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트
|
|
||||||
* label-basic 컴포넌트입니다
|
|
||||||
*/
|
|
||||||
export const LabelBasicComponent: React.FC<LabelBasicComponentProps> = ({
|
|
||||||
component,
|
|
||||||
isDesignMode = false,
|
|
||||||
isSelected = false,
|
|
||||||
onClick,
|
|
||||||
onDragStart,
|
|
||||||
onDragEnd,
|
|
||||||
config,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
// 컴포넌트 설정
|
|
||||||
const componentConfig = {
|
|
||||||
...config,
|
|
||||||
...component.config,
|
|
||||||
} as LabelBasicConfig;
|
|
||||||
|
|
||||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
|
||||||
const componentStyle: React.CSSProperties = {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
...component.style,
|
|
||||||
...style,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 디자인 모드 스타일
|
|
||||||
if (isDesignMode) {
|
|
||||||
componentStyle.border = "1px dashed #cbd5e1";
|
|
||||||
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 핸들러
|
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onClick?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// DOM에 전달하면 안 되는 React-specific props 필터링
|
|
||||||
const {
|
|
||||||
selectedScreen,
|
|
||||||
onZoneComponentDrop,
|
|
||||||
onZoneClick,
|
|
||||||
componentConfig: _componentConfig,
|
|
||||||
component: _component,
|
|
||||||
isSelected: _isSelected,
|
|
||||||
onClick: _onClick,
|
|
||||||
onDragStart: _onDragStart,
|
|
||||||
onDragEnd: _onDragEnd,
|
|
||||||
size: _size,
|
|
||||||
position: _position,
|
|
||||||
style: _style,
|
|
||||||
...domProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={componentStyle} className={className} {...domProps}>
|
|
||||||
{/* 라벨 렌더링 */}
|
|
||||||
{component.label && (
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "-25px",
|
|
||||||
left: "0px",
|
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
|
||||||
color: component.style?.labelColor || "#374151",
|
|
||||||
fontWeight: "500",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{component.label}
|
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={component.value || ""}
|
|
||||||
placeholder={componentConfig.placeholder || ""}
|
|
||||||
disabled={componentConfig.disabled || false}
|
|
||||||
required={componentConfig.required || false}
|
|
||||||
readOnly={componentConfig.readonly || false}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
border: "1px solid #d1d5db",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: "8px 12px",
|
|
||||||
fontSize: "14px",
|
|
||||||
outline: "none",
|
|
||||||
}}
|
|
||||||
onClick={handleClick}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (props.onChange) {
|
|
||||||
props.onChange(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 래퍼 컴포넌트
|
|
||||||
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
|
||||||
*/
|
|
||||||
export const LabelBasicWrapper: React.FC<LabelBasicComponentProps> = (props) => {
|
|
||||||
return <LabelBasicComponent {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { LabelBasicConfig } from "./types";
|
|
||||||
|
|
||||||
export interface LabelBasicConfigPanelProps {
|
|
||||||
config: LabelBasicConfig;
|
|
||||||
onChange: (config: Partial<LabelBasicConfig>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 설정 패널
|
|
||||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
|
||||||
*/
|
|
||||||
export const LabelBasicConfigPanel: React.FC<LabelBasicConfigPanelProps> = ({
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
const handleChange = (key: keyof LabelBasicConfig, value: any) => {
|
|
||||||
onChange({ [key]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="text-sm font-medium">
|
|
||||||
label-basic 설정
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 텍스트 관련 설정 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="placeholder">플레이스홀더</Label>
|
|
||||||
<Input
|
|
||||||
id="placeholder"
|
|
||||||
value={config.placeholder || ""}
|
|
||||||
onChange={(e) => handleChange("placeholder", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="maxLength">최대 길이</Label>
|
|
||||||
<Input
|
|
||||||
id="maxLength"
|
|
||||||
type="number"
|
|
||||||
value={config.maxLength || ""}
|
|
||||||
onChange={(e) => handleChange("maxLength", parseInt(e.target.value) || undefined)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 공통 설정 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="disabled">비활성화</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="disabled"
|
|
||||||
checked={config.disabled || false}
|
|
||||||
onCheckedChange={(checked) => handleChange("disabled", checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="required">필수 입력</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="required"
|
|
||||||
checked={config.required || false}
|
|
||||||
onCheckedChange={(checked) => handleChange("required", checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="readonly">읽기 전용</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="readonly"
|
|
||||||
checked={config.readonly || false}
|
|
||||||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
||||||
import { LabelBasicDefinition } from "./index";
|
|
||||||
import { LabelBasicComponent } from "./LabelBasicComponent";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 렌더러
|
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
|
||||||
*/
|
|
||||||
export class LabelBasicRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = LabelBasicDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
|
||||||
return <LabelBasicComponent {...this.props} renderer={this} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 컴포넌트별 특화 메서드들
|
|
||||||
*/
|
|
||||||
|
|
||||||
// text 타입 특화 속성 처리
|
|
||||||
protected getLabelBasicProps() {
|
|
||||||
const baseProps = this.getWebTypeProps();
|
|
||||||
|
|
||||||
// text 타입에 특화된 추가 속성들
|
|
||||||
return {
|
|
||||||
...baseProps,
|
|
||||||
// 여기에 text 타입 특화 속성들 추가
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 값 변경 처리
|
|
||||||
protected handleValueChange = (value: any) => {
|
|
||||||
this.updateComponent({ value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 포커스 처리
|
|
||||||
protected handleFocus = () => {
|
|
||||||
// 포커스 로직
|
|
||||||
};
|
|
||||||
|
|
||||||
// 블러 처리
|
|
||||||
protected handleBlur = () => {
|
|
||||||
// 블러 로직
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
LabelBasicRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
LabelBasicRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# LabelBasic 컴포넌트
|
|
||||||
|
|
||||||
label-basic 컴포넌트입니다
|
|
||||||
|
|
||||||
## 개요
|
|
||||||
|
|
||||||
- **ID**: `label-basic`
|
|
||||||
- **카테고리**: display
|
|
||||||
- **웹타입**: text
|
|
||||||
- **작성자**: 개발팀
|
|
||||||
- **버전**: 1.0.0
|
|
||||||
|
|
||||||
## 특징
|
|
||||||
|
|
||||||
- ✅ 자동 등록 시스템
|
|
||||||
- ✅ 타입 안전성
|
|
||||||
- ✅ Hot Reload 지원
|
|
||||||
- ✅ 설정 패널 제공
|
|
||||||
- ✅ 반응형 디자인
|
|
||||||
|
|
||||||
## 사용법
|
|
||||||
|
|
||||||
### 기본 사용법
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { LabelBasicComponent } from "@/lib/registry/components/label-basic";
|
|
||||||
|
|
||||||
<LabelBasicComponent
|
|
||||||
component={{
|
|
||||||
id: "my-label-basic",
|
|
||||||
type: "widget",
|
|
||||||
webType: "text",
|
|
||||||
position: { x: 100, y: 100, z: 1 },
|
|
||||||
size: { width: 150, height: 24 },
|
|
||||||
config: {
|
|
||||||
// 설정값들
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isDesignMode={false}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 설정 옵션
|
|
||||||
|
|
||||||
| 속성 | 타입 | 기본값 | 설명 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| placeholder | string | "" | 플레이스홀더 텍스트 |
|
|
||||||
| maxLength | number | 255 | 최대 입력 길이 |
|
|
||||||
| minLength | number | 0 | 최소 입력 길이 |
|
|
||||||
| disabled | boolean | false | 비활성화 여부 |
|
|
||||||
| required | boolean | false | 필수 입력 여부 |
|
|
||||||
| readonly | boolean | false | 읽기 전용 여부 |
|
|
||||||
|
|
||||||
## 이벤트
|
|
||||||
|
|
||||||
- `onChange`: 값 변경 시
|
|
||||||
- `onFocus`: 포커스 시
|
|
||||||
- `onBlur`: 포커스 해제 시
|
|
||||||
- `onClick`: 클릭 시
|
|
||||||
|
|
||||||
## 스타일링
|
|
||||||
|
|
||||||
컴포넌트는 다음과 같은 스타일 옵션을 제공합니다:
|
|
||||||
|
|
||||||
- `variant`: "default" | "outlined" | "filled"
|
|
||||||
- `size`: "sm" | "md" | "lg"
|
|
||||||
|
|
||||||
## 예시
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 기본 예시
|
|
||||||
<LabelBasicComponent
|
|
||||||
component={{
|
|
||||||
id: "sample-label-basic",
|
|
||||||
config: {
|
|
||||||
placeholder: "입력하세요",
|
|
||||||
required: true,
|
|
||||||
variant: "outlined"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 개발자 정보
|
|
||||||
|
|
||||||
- **생성일**: 2025-09-11
|
|
||||||
- **CLI 명령어**: `node scripts/create-component.js label-basic --category=display --webType=text`
|
|
||||||
- **경로**: `lib/registry/components/label-basic/`
|
|
||||||
|
|
||||||
## 관련 문서
|
|
||||||
|
|
||||||
- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md)
|
|
||||||
- [개발자 문서](https://docs.example.com/components/label-basic)
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { LabelBasicConfig } from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트 기본 설정
|
|
||||||
*/
|
|
||||||
export const LabelBasicDefaultConfig: LabelBasicConfig = {
|
|
||||||
placeholder: "텍스트를 입력하세요",
|
|
||||||
maxLength: 255,
|
|
||||||
|
|
||||||
// 공통 기본값
|
|
||||||
disabled: false,
|
|
||||||
required: false,
|
|
||||||
readonly: false,
|
|
||||||
variant: "default",
|
|
||||||
size: "md",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트 설정 스키마
|
|
||||||
* 유효성 검사 및 타입 체크에 사용
|
|
||||||
*/
|
|
||||||
export const LabelBasicConfigSchema = {
|
|
||||||
placeholder: { type: "string", default: "" },
|
|
||||||
maxLength: { type: "number", min: 1 },
|
|
||||||
minLength: { type: "number", min: 0 },
|
|
||||||
|
|
||||||
// 공통 스키마
|
|
||||||
disabled: { type: "boolean", default: false },
|
|
||||||
required: { type: "boolean", default: false },
|
|
||||||
readonly: { type: "boolean", default: false },
|
|
||||||
variant: {
|
|
||||||
type: "enum",
|
|
||||||
values: ["default", "outlined", "filled"],
|
|
||||||
default: "default"
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: "enum",
|
|
||||||
values: ["sm", "md", "lg"],
|
|
||||||
default: "md"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
||||||
import { ComponentCategory } from "@/types/component";
|
|
||||||
import type { WebType } from "@/types/screen";
|
|
||||||
import { LabelBasicWrapper } from "./LabelBasicComponent";
|
|
||||||
import { LabelBasicConfigPanel } from "./LabelBasicConfigPanel";
|
|
||||||
import { LabelBasicConfig } from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트 정의
|
|
||||||
* label-basic 컴포넌트입니다
|
|
||||||
*/
|
|
||||||
export const LabelBasicDefinition = createComponentDefinition({
|
|
||||||
id: "label-basic",
|
|
||||||
name: "라벨 텍스트",
|
|
||||||
nameEng: "LabelBasic Component",
|
|
||||||
description: "텍스트 표시를 위한 라벨 텍스트 컴포넌트",
|
|
||||||
category: ComponentCategory.DISPLAY,
|
|
||||||
webType: "text",
|
|
||||||
component: LabelBasicWrapper,
|
|
||||||
defaultConfig: {
|
|
||||||
placeholder: "텍스트를 입력하세요",
|
|
||||||
maxLength: 255,
|
|
||||||
},
|
|
||||||
defaultSize: { width: 150, height: 24 },
|
|
||||||
configPanel: LabelBasicConfigPanel,
|
|
||||||
icon: "Eye",
|
|
||||||
tags: [],
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "개발팀",
|
|
||||||
documentation: "https://docs.example.com/components/label-basic",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 타입 내보내기
|
|
||||||
export type { LabelBasicConfig } from "./types";
|
|
||||||
|
|
||||||
// 컴포넌트 내보내기
|
|
||||||
export { LabelBasicComponent } from "./LabelBasicComponent";
|
|
||||||
export { LabelBasicRenderer } from "./LabelBasicRenderer";
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ComponentConfig } from "@/types/component";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트 설정 타입
|
|
||||||
*/
|
|
||||||
export interface LabelBasicConfig extends ComponentConfig {
|
|
||||||
// 텍스트 관련 설정
|
|
||||||
placeholder?: string;
|
|
||||||
maxLength?: number;
|
|
||||||
minLength?: number;
|
|
||||||
|
|
||||||
// 공통 설정
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
readonly?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
helperText?: string;
|
|
||||||
|
|
||||||
// 스타일 관련
|
|
||||||
variant?: "default" | "outlined" | "filled";
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
|
|
||||||
// 이벤트 관련
|
|
||||||
onChange?: (value: any) => void;
|
|
||||||
onFocus?: () => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LabelBasic 컴포넌트 Props 타입
|
|
||||||
*/
|
|
||||||
export interface LabelBasicProps {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
value?: any;
|
|
||||||
config?: LabelBasicConfig;
|
|
||||||
className?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
|
|
||||||
// 이벤트 핸들러
|
|
||||||
onChange?: (value: any) => void;
|
|
||||||
onFocus?: () => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,10 +86,20 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && (
|
||||||
|
<span style={{
|
||||||
|
color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -96,15 +113,15 @@ export const NumberInputComponent: React.FC<NumberInputComponentProps> = ({
|
||||||
min={componentConfig.min}
|
min={componentConfig.min}
|
||||||
max={componentConfig.max}
|
max={componentConfig.max}
|
||||||
step={componentConfig.step || 1}
|
step={componentConfig.step || 1}
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
border: "1px solid #d1d5db",
|
border: "1px solid #d1d5db",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,22 +86,28 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: componentConfig.direction === "horizontal" ? "row" : "column",
|
flexDirection: componentConfig.direction === "horizontal" ? "row" : "column",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
@ -102,13 +115,14 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
{(componentConfig.options || []).map((option, index) => (
|
{(componentConfig.options || []).map((option, index) => (
|
||||||
<label
|
<label
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{display: "flex",
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "6px",
|
gap: "6px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|
@ -117,37 +131,53 @@ export const RadioBasicComponent: React.FC<RadioBasicComponentProps> = ({
|
||||||
checked={component.value === option.value}
|
checked={component.value === option.value}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
style={{
|
style={{width: "16px",
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
height: "16px",
|
||||||
accentColor: "#3b82f6",
|
accentColor: "#3b82f6",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (component.onChange) {
|
if (component.onChange) {
|
||||||
component.onChange(e.target.value);
|
component.onChange(e.target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span style={{ color: "#374151" }}>{option.label}</span>
|
<span style={{color: "#374151",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{(!componentConfig.options || componentConfig.options.length === 0) && (
|
{(!componentConfig.options || componentConfig.options.length === 0) && (
|
||||||
<>
|
<>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: "pointer", fontSize: "14px" }}>
|
<label style={{display: "flex", alignItems: "center", gap: "6px", cursor: "pointer", fontSize: "14px",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name={component.id || "radio-group"}
|
name={component.id || "radio-group"}
|
||||||
value="option1"
|
value="option1"
|
||||||
style={{ width: "16px", height: "16px", accentColor: "#3b82f6" }}
|
style={{width: "16px", height: "16px", accentColor: "#3b82f6",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<span>옵션 1</span>
|
<span>옵션 1</span>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: "pointer", fontSize: "14px" }}>
|
<label style={{display: "flex", alignItems: "center", gap: "6px", cursor: "pointer", fontSize: "14px",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name={component.id || "radio-group"}
|
name={component.id || "radio-group"}
|
||||||
value="option2"
|
value="option2"
|
||||||
style={{ width: "16px", height: "16px", accentColor: "#3b82f6" }}
|
style={{width: "16px", height: "16px", accentColor: "#3b82f6",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<span>옵션 2</span>
|
<span>옵션 2</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,10 +86,15 @@ export const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -91,8 +103,7 @@ export const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
multiple={componentConfig.multiple || false}
|
multiple={componentConfig.multiple || false}
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
border: "1px solid #d1d5db",
|
border: "1px solid #d1d5db",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
|
@ -100,7 +111,9 @@ export const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,22 +86,28 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "12px",
|
gap: "12px",
|
||||||
padding: "8px",
|
padding: "8px",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
@ -107,13 +120,14 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
||||||
value={component.value || componentConfig.min || 0}
|
value={component.value || componentConfig.min || 0}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
style={{
|
style={{width: "70%",
|
||||||
width: "70%",
|
|
||||||
height: "6px",
|
height: "6px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
borderRadius: "3px",
|
borderRadius: "3px",
|
||||||
background: "#e5e7eb",
|
background: "#e5e7eb",
|
||||||
accentColor: "#3b82f6",
|
accentColor: "#3b82f6",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (component.onChange) {
|
if (component.onChange) {
|
||||||
|
|
@ -122,12 +136,13 @@ export const SliderBasicComponent: React.FC<SliderBasicComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{width: "30%",
|
||||||
width: "30%",
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
color: "#374151",
|
color: "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.value || componentConfig.min || 0}
|
{component.value || componentConfig.min || 0}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
# TextDisplay 컴포넌트
|
||||||
|
|
||||||
|
텍스트 표시 전용 컴포넌트입니다
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
- **ID**: `text-display`
|
||||||
|
- **카테고리**: display
|
||||||
|
- **웹타입**: text
|
||||||
|
- **작성자**: 개발팀
|
||||||
|
- **버전**: 1.0.0
|
||||||
|
|
||||||
|
## 특징
|
||||||
|
|
||||||
|
- ✅ 자동 등록 시스템
|
||||||
|
- ✅ 타입 안전성
|
||||||
|
- ✅ Hot Reload 지원
|
||||||
|
- ✅ 설정 패널 제공
|
||||||
|
- ✅ 반응형 디자인
|
||||||
|
- ✅ 다양한 텍스트 스타일 옵션
|
||||||
|
|
||||||
|
## 사용법
|
||||||
|
|
||||||
|
### 기본 사용법
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { TextDisplayComponent } from "@/lib/registry/components/text-display";
|
||||||
|
|
||||||
|
<TextDisplayComponent
|
||||||
|
component={{
|
||||||
|
id: "my-text-display",
|
||||||
|
type: "widget",
|
||||||
|
webType: "text",
|
||||||
|
position: { x: 100, y: 100, z: 1 },
|
||||||
|
size: { width: 150, height: 24 },
|
||||||
|
componentConfig: {
|
||||||
|
text: "표시할 텍스트",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: "#333333"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDesignMode={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 설정 옵션
|
||||||
|
|
||||||
|
| 속성 | 타입 | 기본값 | 설명 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| text | string | "텍스트를 입력하세요" | 표시할 텍스트 |
|
||||||
|
| fontSize | string | "14px" | 폰트 크기 |
|
||||||
|
| fontWeight | string | "normal" | 폰트 굵기 |
|
||||||
|
| color | string | "#374151" | 텍스트 색상 |
|
||||||
|
| textAlign | "left" \| "center" \| "right" | "left" | 텍스트 정렬 |
|
||||||
|
| backgroundColor | string | "transparent" | 배경색 |
|
||||||
|
| padding | string | "0" | 패딩 |
|
||||||
|
| borderRadius | string | "0" | 모서리 둥글기 |
|
||||||
|
| border | string | "none" | 테두리 |
|
||||||
|
| disabled | boolean | false | 비활성화 여부 |
|
||||||
|
|
||||||
|
## 이벤트
|
||||||
|
|
||||||
|
- `onClick`: 클릭 시 (실제 모드에서만)
|
||||||
|
|
||||||
|
## 스타일링
|
||||||
|
|
||||||
|
컴포넌트는 다음과 같은 스타일 옵션을 제공합니다:
|
||||||
|
|
||||||
|
- **폰트 크기**: px, em, rem 단위 지원
|
||||||
|
- **폰트 굵기**: normal, bold, 100-900 숫자값
|
||||||
|
- **텍스트 정렬**: 왼쪽, 가운데, 오른쪽
|
||||||
|
- **색상**: HEX, RGB, 색상명 지원
|
||||||
|
- **배경 및 테두리**: CSS 표준 속성 지원
|
||||||
|
|
||||||
|
## 예시
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 제목 텍스트
|
||||||
|
<TextDisplayComponent
|
||||||
|
component={{
|
||||||
|
id: "title-text",
|
||||||
|
componentConfig: {
|
||||||
|
text: "제품 관리 시스템",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#1f2937",
|
||||||
|
textAlign: "center"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 설명 텍스트
|
||||||
|
<TextDisplayComponent
|
||||||
|
component={{
|
||||||
|
id: "description-text",
|
||||||
|
componentConfig: {
|
||||||
|
text: "제품 정보를 관리할 수 있습니다.",
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#6b7280",
|
||||||
|
backgroundColor: "#f9fafb",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "4px"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 라벨 텍스트 컴포넌트와의 차이점
|
||||||
|
|
||||||
|
기존 `label-basic` 컴포넌트를 대체하는 새로운 컴포넌트입니다:
|
||||||
|
|
||||||
|
### 개선사항
|
||||||
|
- ✅ 더 직관적인 설정 패널
|
||||||
|
- ✅ 다양한 텍스트 스타일 옵션
|
||||||
|
- ✅ 실시간 텍스트 편집
|
||||||
|
- ✅ 더 나은 사용자 경험
|
||||||
|
|
||||||
|
### 마이그레이션
|
||||||
|
기존 `label-basic` 컴포넌트는 자동으로 `text-display`로 교체됩니다.
|
||||||
|
|
||||||
|
## 개발자 정보
|
||||||
|
|
||||||
|
- **생성일**: 2025-09-12
|
||||||
|
- **교체 대상**: label-basic 컴포넌트
|
||||||
|
- **경로**: `lib/registry/components/text-display/`
|
||||||
|
|
||||||
|
## 관련 문서
|
||||||
|
|
||||||
|
- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md)
|
||||||
|
- [개발자 문서](https://docs.example.com/components/text-display)
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { ComponentRendererProps } from "../../types";
|
||||||
|
import { TextDisplayConfig } from "./types";
|
||||||
|
|
||||||
|
export interface TextDisplayComponentProps extends ComponentRendererProps {
|
||||||
|
// 추가 props가 필요한 경우 여기에 정의
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 컴포넌트
|
||||||
|
* text-display 컴포넌트입니다
|
||||||
|
*/
|
||||||
|
export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
||||||
|
component,
|
||||||
|
isDesignMode = false,
|
||||||
|
isSelected = false,
|
||||||
|
onClick,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const componentConfig = (component.componentConfig || {}) as TextDisplayConfig;
|
||||||
|
|
||||||
|
// 컴포넌트 스타일 계산
|
||||||
|
const componentStyle: React.CSSProperties = {
|
||||||
|
position: "absolute",
|
||||||
|
left: `${component.style?.positionX || 0}px`,
|
||||||
|
top: `${component.style?.positionY || 0}px`,
|
||||||
|
width: `${component.style?.width || 150}px`,
|
||||||
|
height: `${component.style?.height || 24}px`,
|
||||||
|
zIndex: component.style?.positionZ || 1,
|
||||||
|
cursor: isDesignMode ? "pointer" : "default",
|
||||||
|
border: isSelected ? "2px solid #3b82f6" : "none",
|
||||||
|
outline: isSelected ? "none" : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 클릭 핸들러
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (isDesignMode) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick?.(e);
|
||||||
|
} else {
|
||||||
|
// 실제 모드에서의 클릭 처리
|
||||||
|
componentConfig.onClick?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// className 생성
|
||||||
|
const className = ["text-display-component", isSelected ? "selected" : "", componentConfig.disabled ? "disabled" : ""]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
// DOM props 필터링 (React 관련 props 제거)
|
||||||
|
const {
|
||||||
|
component: _component,
|
||||||
|
isDesignMode: _isDesignMode,
|
||||||
|
isSelected: _isSelected,
|
||||||
|
isInteractive: _isInteractive,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
|
formData: _formData,
|
||||||
|
onFormDataChange: _onFormDataChange,
|
||||||
|
componentConfig: _componentConfig,
|
||||||
|
...domProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// 텍스트 스타일 계산
|
||||||
|
const textStyle: React.CSSProperties = {
|
||||||
|
fontSize: componentConfig.fontSize || "14px",
|
||||||
|
fontWeight: componentConfig.fontWeight || "normal",
|
||||||
|
color: componentConfig.color || "#374151",
|
||||||
|
textAlign: componentConfig.textAlign || "left",
|
||||||
|
backgroundColor: componentConfig.backgroundColor || "transparent",
|
||||||
|
padding: componentConfig.padding || "0",
|
||||||
|
borderRadius: componentConfig.borderRadius || "0",
|
||||||
|
border: componentConfig.border || "none",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent:
|
||||||
|
componentConfig.textAlign === "center"
|
||||||
|
? "center"
|
||||||
|
: componentConfig.textAlign === "right"
|
||||||
|
? "flex-end"
|
||||||
|
: "flex-start",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
overflow: "hidden",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={componentStyle} className={className} {...domProps}>
|
||||||
|
{/* 라벨 렌더링 */}
|
||||||
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-25px",
|
||||||
|
left: "0px",
|
||||||
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
|
color: component.style?.labelColor || "#374151",
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{component.label}
|
||||||
|
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={textStyle} onClick={handleClick} onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||||
|
{componentConfig.text || "텍스트를 입력하세요"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 래퍼 컴포넌트
|
||||||
|
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
||||||
|
*/
|
||||||
|
export const TextDisplayWrapper: React.FC<TextDisplayComponentProps> = (props) => {
|
||||||
|
return <TextDisplayComponent {...props} />;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { TextDisplayConfig } from "./types";
|
||||||
|
|
||||||
|
export interface TextDisplayConfigPanelProps {
|
||||||
|
config: TextDisplayConfig;
|
||||||
|
onChange: (config: Partial<TextDisplayConfig>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 설정 패널
|
||||||
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||||
|
*/
|
||||||
|
export const TextDisplayConfigPanel: React.FC<TextDisplayConfigPanelProps> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const handleChange = (key: keyof TextDisplayConfig, value: any) => {
|
||||||
|
onChange({ [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
텍스트 표시 설정
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 표시할 텍스트 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="text">표시할 텍스트</Label>
|
||||||
|
<Input
|
||||||
|
id="text"
|
||||||
|
value={config.text || ""}
|
||||||
|
onChange={(e) => handleChange("text", e.target.value)}
|
||||||
|
placeholder="표시할 텍스트를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 폰트 크기 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="fontSize">폰트 크기</Label>
|
||||||
|
<Input
|
||||||
|
id="fontSize"
|
||||||
|
value={config.fontSize || ""}
|
||||||
|
onChange={(e) => handleChange("fontSize", e.target.value)}
|
||||||
|
placeholder="14px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 폰트 굵기 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="fontWeight">폰트 굵기</Label>
|
||||||
|
<Select
|
||||||
|
value={config.fontWeight || "normal"}
|
||||||
|
onValueChange={(value) => handleChange("fontWeight", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="폰트 굵기 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="normal">보통</SelectItem>
|
||||||
|
<SelectItem value="bold">굵게</SelectItem>
|
||||||
|
<SelectItem value="lighter">얇게</SelectItem>
|
||||||
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
<SelectItem value="200">200</SelectItem>
|
||||||
|
<SelectItem value="300">300</SelectItem>
|
||||||
|
<SelectItem value="400">400</SelectItem>
|
||||||
|
<SelectItem value="500">500</SelectItem>
|
||||||
|
<SelectItem value="600">600</SelectItem>
|
||||||
|
<SelectItem value="700">700</SelectItem>
|
||||||
|
<SelectItem value="800">800</SelectItem>
|
||||||
|
<SelectItem value="900">900</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 텍스트 색상 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="color">텍스트 색상</Label>
|
||||||
|
<Input
|
||||||
|
id="color"
|
||||||
|
type="color"
|
||||||
|
value={config.color || "#374151"}
|
||||||
|
onChange={(e) => handleChange("color", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 텍스트 정렬 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="textAlign">텍스트 정렬</Label>
|
||||||
|
<Select
|
||||||
|
value={config.textAlign || "left"}
|
||||||
|
onValueChange={(value) => handleChange("textAlign", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="정렬 방식 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">왼쪽 정렬</SelectItem>
|
||||||
|
<SelectItem value="center">가운데 정렬</SelectItem>
|
||||||
|
<SelectItem value="right">오른쪽 정렬</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 배경색 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="backgroundColor">배경색</Label>
|
||||||
|
<Input
|
||||||
|
id="backgroundColor"
|
||||||
|
type="color"
|
||||||
|
value={config.backgroundColor || "#ffffff"}
|
||||||
|
onChange={(e) => handleChange("backgroundColor", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 패딩 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="padding">패딩</Label>
|
||||||
|
<Input
|
||||||
|
id="padding"
|
||||||
|
value={config.padding || ""}
|
||||||
|
onChange={(e) => handleChange("padding", e.target.value)}
|
||||||
|
placeholder="8px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모서리 둥글기 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="borderRadius">모서리 둥글기</Label>
|
||||||
|
<Input
|
||||||
|
id="borderRadius"
|
||||||
|
value={config.borderRadius || ""}
|
||||||
|
onChange={(e) => handleChange("borderRadius", e.target.value)}
|
||||||
|
placeholder="4px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테두리 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="border">테두리</Label>
|
||||||
|
<Input
|
||||||
|
id="border"
|
||||||
|
value={config.border || ""}
|
||||||
|
onChange={(e) => handleChange("border", e.target.value)}
|
||||||
|
placeholder="1px solid #d1d5db"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 비활성화 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="disabled">비활성화</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="disabled"
|
||||||
|
checked={config.disabled || false}
|
||||||
|
onCheckedChange={(checked) => handleChange("disabled", checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||||
|
import { TextDisplayDefinition } from "./index";
|
||||||
|
import { TextDisplayComponent } from "./TextDisplayComponent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 렌더러
|
||||||
|
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||||
|
*/
|
||||||
|
export class TextDisplayRenderer extends AutoRegisteringComponentRenderer {
|
||||||
|
static componentDefinition = TextDisplayDefinition;
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return <TextDisplayComponent {...this.props} renderer={this} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트별 특화 메서드들
|
||||||
|
*/
|
||||||
|
|
||||||
|
// text 타입 특화 속성 처리
|
||||||
|
protected getTextDisplayProps() {
|
||||||
|
const baseProps = this.getWebTypeProps();
|
||||||
|
|
||||||
|
// text 타입에 특화된 추가 속성들
|
||||||
|
return {
|
||||||
|
...baseProps,
|
||||||
|
// 여기에 text 타입 특화 속성들 추가
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클릭 처리
|
||||||
|
protected handleClick = () => {
|
||||||
|
// 클릭 로직
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동 등록 실행
|
||||||
|
TextDisplayRenderer.registerSelf();
|
||||||
|
|
||||||
|
// Hot Reload는 Next.js에서 자동으로 처리됨
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||||
|
import { ComponentCategory } from "@/types/component";
|
||||||
|
import type { WebType } from "@/types/screen";
|
||||||
|
import { TextDisplayWrapper } from "./TextDisplayComponent";
|
||||||
|
import { TextDisplayConfigPanel } from "./TextDisplayConfigPanel";
|
||||||
|
import { TextDisplayConfig } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 컴포넌트 정의
|
||||||
|
* text-display 컴포넌트입니다
|
||||||
|
*/
|
||||||
|
export const TextDisplayDefinition = createComponentDefinition({
|
||||||
|
id: "text-display",
|
||||||
|
name: "텍스트 표시",
|
||||||
|
nameEng: "TextDisplay Component",
|
||||||
|
description: "텍스트를 표시하기 위한 컴포넌트",
|
||||||
|
category: ComponentCategory.DISPLAY,
|
||||||
|
webType: "text",
|
||||||
|
component: TextDisplayWrapper,
|
||||||
|
defaultConfig: {
|
||||||
|
text: "텍스트를 입력하세요",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "normal",
|
||||||
|
color: "#374151",
|
||||||
|
textAlign: "left",
|
||||||
|
},
|
||||||
|
defaultSize: { width: 150, height: 24 },
|
||||||
|
configPanel: TextDisplayConfigPanel,
|
||||||
|
icon: "Type",
|
||||||
|
tags: ["텍스트", "표시", "라벨"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "개발팀",
|
||||||
|
documentation: "https://docs.example.com/components/text-display",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 타입 내보내기
|
||||||
|
export type { TextDisplayConfig } from "./types";
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ComponentConfig } from "@/types/component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 컴포넌트 설정 타입
|
||||||
|
*/
|
||||||
|
export interface TextDisplayConfig extends ComponentConfig {
|
||||||
|
// 텍스트 관련 설정
|
||||||
|
text?: string; // 표시할 텍스트
|
||||||
|
fontSize?: string; // 폰트 크기
|
||||||
|
fontWeight?: string; // 폰트 굵기
|
||||||
|
color?: string; // 텍스트 색상
|
||||||
|
textAlign?: "left" | "center" | "right"; // 텍스트 정렬
|
||||||
|
|
||||||
|
// 스타일 관련
|
||||||
|
backgroundColor?: string; // 배경색
|
||||||
|
padding?: string; // 패딩
|
||||||
|
borderRadius?: string; // 모서리 둥글기
|
||||||
|
border?: string; // 테두리
|
||||||
|
|
||||||
|
// 공통 설정
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
// 이벤트 관련
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDisplay 컴포넌트 Props 타입
|
||||||
|
*/
|
||||||
|
export interface TextDisplayProps {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: any;
|
||||||
|
config?: TextDisplayConfig;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
|
// 이벤트 핸들러
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,15 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -30,6 +33,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
...component.config,
|
...component.config,
|
||||||
} as TextInputConfig;
|
} as TextInputConfig;
|
||||||
|
|
||||||
|
console.log("🔧 텍스트 입력 컴포넌트 설정:", {
|
||||||
|
config,
|
||||||
|
componentConfig,
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
|
||||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||||
const componentStyle: React.CSSProperties = {
|
const componentStyle: React.CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -64,6 +73,10 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -88,7 +101,11 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={component.value || ""}
|
value={
|
||||||
|
isInteractive && formData && component.columnName
|
||||||
|
? formData[component.columnName] || ""
|
||||||
|
: component.value || ""
|
||||||
|
}
|
||||||
placeholder={componentConfig.placeholder || ""}
|
placeholder={componentConfig.placeholder || ""}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
|
|
@ -101,13 +118,23 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
|
||||||
|
// isInteractive 모드에서는 formData 업데이트
|
||||||
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
|
onFormDataChange(component.columnName, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 onChange 핸들러도 호출
|
||||||
if (props.onChange) {
|
if (props.onChange) {
|
||||||
props.onChange(e.target.value);
|
props.onChange(newValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,7 @@ export const TextInputDefinition = createComponentDefinition({
|
||||||
documentation: "https://docs.example.com/components/text-input",
|
documentation: "https://docs.example.com/components/text-input",
|
||||||
});
|
});
|
||||||
|
|
||||||
// ComponentRegistry에 등록
|
// 컴포넌트는 TextInputRenderer에서 자동 등록됩니다
|
||||||
import { ComponentRegistry } from "../../ComponentRegistry";
|
|
||||||
ComponentRegistry.registerComponent(TextInputDefinition);
|
|
||||||
|
|
||||||
console.log("🚀 TextInput 컴포넌트 등록 완료");
|
|
||||||
|
|
||||||
// 타입 내보내기
|
// 타입 내보내기
|
||||||
export type { TextInputConfig } from "./types";
|
export type { TextInputConfig } from "./types";
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,10 +86,15 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -93,8 +105,7 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
readOnly={componentConfig.readonly || false}
|
readOnly={componentConfig.readonly || false}
|
||||||
rows={componentConfig.rows || 3}
|
rows={componentConfig.rows || 3}
|
||||||
style={{
|
style={{width: "100%",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
border: "1px solid #d1d5db",
|
border: "1px solid #d1d5db",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
|
@ -102,7 +113,9 @@ export const TextareaBasicComponent: React.FC<TextareaBasicComponentProps> = ({
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
outline: "none",
|
outline: "none",
|
||||||
resize: "none",
|
resize: "none",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
|
|
@ -64,6 +67,10 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
size: _size,
|
size: _size,
|
||||||
position: _position,
|
position: _position,
|
||||||
style: _style,
|
style: _style,
|
||||||
|
screenId: _screenId,
|
||||||
|
tableName: _tableName,
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
onClose: _onClose,
|
||||||
...domProps
|
...domProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -79,30 +86,35 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#374151",
|
color: component.style?.labelColor || "#374151",
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span style={{ color: "#ef4444" }}>*</span>}
|
{component.required && <span style={{color: "#ef4444",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label
|
<label
|
||||||
style={{
|
style={{display: "flex",
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "12px",
|
gap: "12px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{position: "relative",
|
||||||
position: "relative",
|
|
||||||
width: "48px",
|
width: "48px",
|
||||||
height: "24px",
|
height: "24px",
|
||||||
backgroundColor: component.value === true ? "#3b82f6" : "#d1d5db",
|
backgroundColor: component.value === true ? "#3b82f6" : "#d1d5db",
|
||||||
|
|
@ -110,6 +122,8 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
transition: "background-color 0.2s",
|
transition: "background-color 0.2s",
|
||||||
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
cursor: componentConfig.disabled ? "not-allowed" : "pointer",
|
||||||
opacity: componentConfig.disabled ? 0.5 : 1,
|
opacity: componentConfig.disabled ? 0.5 : 1,
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -118,12 +132,14 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (component.onChange) {
|
if (component.onChange) {
|
||||||
component.onChange(e.target.checked);
|
component.onChange(e.target.checked);
|
||||||
|
|
@ -132,7 +148,7 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "2px",
|
top: "2px",
|
||||||
left: component.value === true ? "26px" : "2px",
|
left: component.value === true ? "26px" : "2px",
|
||||||
width: "20px",
|
width: "20px",
|
||||||
|
|
@ -141,10 +157,15 @@ export const ToggleSwitchComponent: React.FC<ToggleSwitchComponentProps> = ({
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
transition: "left 0.2s",
|
transition: "left 0.2s",
|
||||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||||
}}
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ color: "#374151" }}>{componentConfig.toggleLabel || (component.value ? "켜짐" : "꺼짐")}</span>
|
<span style={{color: "#374151",
|
||||||
|
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||||
|
...(isInteractive && component.style ? component.style : {}),
|
||||||
|
}}>{componentConfig.toggleLabel || (component.value ? "켜짐" : "꺼짐")}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,498 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버튼 액션 타입 정의
|
||||||
|
*/
|
||||||
|
export type ButtonActionType =
|
||||||
|
| "save" // 저장
|
||||||
|
| "cancel" // 취소
|
||||||
|
| "delete" // 삭제
|
||||||
|
| "edit" // 편집
|
||||||
|
| "add" // 추가
|
||||||
|
| "search" // 검색
|
||||||
|
| "reset" // 초기화
|
||||||
|
| "submit" // 제출
|
||||||
|
| "close" // 닫기
|
||||||
|
| "popup" // 팝업 열기
|
||||||
|
| "navigate" // 페이지 이동
|
||||||
|
| "modal" // 모달 열기
|
||||||
|
| "newWindow"; // 새 창 열기
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버튼 액션 설정
|
||||||
|
*/
|
||||||
|
export interface ButtonActionConfig {
|
||||||
|
type: ButtonActionType;
|
||||||
|
|
||||||
|
// 저장/제출 관련
|
||||||
|
saveEndpoint?: string;
|
||||||
|
validateForm?: boolean;
|
||||||
|
|
||||||
|
// 네비게이션 관련
|
||||||
|
targetUrl?: string;
|
||||||
|
targetScreenId?: number;
|
||||||
|
|
||||||
|
// 모달/팝업 관련
|
||||||
|
modalTitle?: string;
|
||||||
|
modalSize?: "sm" | "md" | "lg" | "xl";
|
||||||
|
popupWidth?: number;
|
||||||
|
popupHeight?: number;
|
||||||
|
|
||||||
|
// 확인 메시지
|
||||||
|
confirmMessage?: string;
|
||||||
|
successMessage?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버튼 액션 실행 컨텍스트
|
||||||
|
*/
|
||||||
|
export interface ButtonActionContext {
|
||||||
|
formData: Record<string, any>;
|
||||||
|
screenId?: number;
|
||||||
|
tableName?: string;
|
||||||
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버튼 액션 실행기
|
||||||
|
*/
|
||||||
|
export class ButtonActionExecutor {
|
||||||
|
/**
|
||||||
|
* 액션 실행
|
||||||
|
*/
|
||||||
|
static async executeAction(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 확인 로직은 컴포넌트에서 처리하므로 여기서는 제거
|
||||||
|
|
||||||
|
switch (config.type) {
|
||||||
|
case "save":
|
||||||
|
return await this.handleSave(config, context);
|
||||||
|
|
||||||
|
case "submit":
|
||||||
|
return await this.handleSubmit(config, context);
|
||||||
|
|
||||||
|
case "delete":
|
||||||
|
return await this.handleDelete(config, context);
|
||||||
|
|
||||||
|
case "reset":
|
||||||
|
return this.handleReset(config, context);
|
||||||
|
|
||||||
|
case "cancel":
|
||||||
|
return this.handleCancel(config, context);
|
||||||
|
|
||||||
|
case "navigate":
|
||||||
|
return this.handleNavigate(config, context);
|
||||||
|
|
||||||
|
case "modal":
|
||||||
|
return this.handleModal(config, context);
|
||||||
|
|
||||||
|
case "newWindow":
|
||||||
|
return this.handleNewWindow(config, context);
|
||||||
|
|
||||||
|
case "popup":
|
||||||
|
return this.handlePopup(config, context);
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
return this.handleSearch(config, context);
|
||||||
|
|
||||||
|
case "add":
|
||||||
|
return this.handleAdd(config, context);
|
||||||
|
|
||||||
|
case "edit":
|
||||||
|
return this.handleEdit(config, context);
|
||||||
|
|
||||||
|
case "close":
|
||||||
|
return this.handleClose(config, context);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("버튼 액션 실행 오류:", error);
|
||||||
|
toast.error(config.errorMessage || "작업 중 오류가 발생했습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 저장 액션 처리
|
||||||
|
*/
|
||||||
|
private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
|
const { formData, tableName, screenId } = context;
|
||||||
|
|
||||||
|
// 폼 유효성 검사
|
||||||
|
if (config.validateForm) {
|
||||||
|
const validation = this.validateFormData(formData);
|
||||||
|
if (!validation.isValid) {
|
||||||
|
toast.error(`입력값을 확인해주세요: ${validation.errors.join(", ")}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API 엔드포인트가 지정된 경우
|
||||||
|
if (config.saveEndpoint) {
|
||||||
|
const response = await fetch(config.saveEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`저장 실패: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
} else if (tableName && screenId) {
|
||||||
|
// 기본 테이블 저장 로직
|
||||||
|
console.log("테이블 저장:", { tableName, formData, screenId });
|
||||||
|
|
||||||
|
// 실제 저장 API 호출
|
||||||
|
const saveResult = await DynamicFormApi.saveFormData({
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
data: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!saveResult.success) {
|
||||||
|
throw new Error(saveResult.message || "저장에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 저장 성공:", saveResult);
|
||||||
|
} else {
|
||||||
|
throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.onRefresh?.();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("저장 오류:", error);
|
||||||
|
throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 제출 액션 처리
|
||||||
|
*/
|
||||||
|
private static async handleSubmit(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
|
// 제출은 저장과 유사하지만 추가적인 처리가 있을 수 있음
|
||||||
|
return await this.handleSave(config, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 삭제 액션 처리
|
||||||
|
*/
|
||||||
|
private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
|
const { formData, tableName, screenId } = context;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (tableName && screenId && formData.id) {
|
||||||
|
console.log("데이터 삭제:", { tableName, screenId, id: formData.id });
|
||||||
|
|
||||||
|
// 실제 삭제 API 호출
|
||||||
|
const deleteResult = await DynamicFormApi.deleteFormData(formData.id);
|
||||||
|
|
||||||
|
if (!deleteResult.success) {
|
||||||
|
throw new Error(deleteResult.message || "삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 삭제 성공:", deleteResult);
|
||||||
|
} else {
|
||||||
|
throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.onRefresh?.();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("삭제 오류:", error);
|
||||||
|
throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초기화 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleReset(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
const { formData, onFormDataChange } = context;
|
||||||
|
|
||||||
|
// 폼 데이터 초기화 - 각 필드를 개별적으로 초기화
|
||||||
|
if (onFormDataChange && formData) {
|
||||||
|
Object.keys(formData).forEach((key) => {
|
||||||
|
onFormDataChange(key, "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(config.successMessage || "초기화되었습니다.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취소 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleCancel(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
const { onClose } = context;
|
||||||
|
|
||||||
|
onClose?.();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네비게이션 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleNavigate(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
let targetUrl = config.targetUrl;
|
||||||
|
|
||||||
|
// 화면 ID가 지정된 경우 URL 생성
|
||||||
|
if (config.targetScreenId) {
|
||||||
|
targetUrl = `/screens/${config.targetScreenId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUrl) {
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error("이동할 페이지가 지정되지 않았습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모달 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleModal(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
// 모달 열기 로직
|
||||||
|
console.log("모달 열기:", {
|
||||||
|
title: config.modalTitle,
|
||||||
|
size: config.modalSize,
|
||||||
|
targetScreenId: config.targetScreenId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.targetScreenId) {
|
||||||
|
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
||||||
|
const modalEvent = new CustomEvent("openScreenModal", {
|
||||||
|
detail: {
|
||||||
|
screenId: config.targetScreenId,
|
||||||
|
title: config.modalTitle || "화면",
|
||||||
|
size: config.modalSize || "md",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.dispatchEvent(modalEvent);
|
||||||
|
toast.success("모달 화면이 열렸습니다.");
|
||||||
|
} else {
|
||||||
|
toast.error("모달로 열 화면이 지정되지 않았습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 새 창 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleNewWindow(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
let targetUrl = config.targetUrl;
|
||||||
|
|
||||||
|
// 화면 ID가 지정된 경우 URL 생성
|
||||||
|
if (config.targetScreenId) {
|
||||||
|
targetUrl = `/screens/${config.targetScreenId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUrl) {
|
||||||
|
const windowFeatures = `width=${config.popupWidth || 800},height=${config.popupHeight || 600},scrollbars=yes,resizable=yes`;
|
||||||
|
window.open(targetUrl, "_blank", windowFeatures);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error("열 페이지가 지정되지 않았습니다.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 팝업 액션 처리
|
||||||
|
*/
|
||||||
|
private static handlePopup(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
// 팝업은 새 창과 유사하지만 더 작은 크기
|
||||||
|
return this.handleNewWindow(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
popupWidth: config.popupWidth || 600,
|
||||||
|
popupHeight: config.popupHeight || 400,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleSearch(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
const { formData, onRefresh } = context;
|
||||||
|
|
||||||
|
console.log("검색 실행:", formData);
|
||||||
|
|
||||||
|
// 검색 조건 검증
|
||||||
|
const hasSearchCriteria = Object.values(formData).some(
|
||||||
|
(value) => value !== null && value !== undefined && value !== "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSearchCriteria) {
|
||||||
|
toast.warning("검색 조건을 입력해주세요.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 실행 (데이터 새로고침)
|
||||||
|
onRefresh?.();
|
||||||
|
|
||||||
|
// 검색 조건을 URL 파라미터로 추가 (선택사항)
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(formData).forEach(([key, value]) => {
|
||||||
|
if (value !== null && value !== undefined && value !== "") {
|
||||||
|
searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL 업데이트 (히스토리에 추가하지 않음)
|
||||||
|
if (searchParams.toString()) {
|
||||||
|
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
|
||||||
|
window.history.replaceState({}, "", newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(config.successMessage || "검색을 실행했습니다.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 추가 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleAdd(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
console.log("추가 액션 실행:", context);
|
||||||
|
// 추가 로직 구현 (예: 새 레코드 생성 폼 열기)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 편집 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleEdit(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
console.log("편집 액션 실행:", context);
|
||||||
|
// 편집 로직 구현 (예: 편집 모드로 전환)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 닫기 액션 처리
|
||||||
|
*/
|
||||||
|
private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean {
|
||||||
|
console.log("닫기 액션 실행:", context);
|
||||||
|
context.onClose?.();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 폼 데이터 유효성 검사
|
||||||
|
*/
|
||||||
|
private static validateFormData(formData: Record<string, any>): {
|
||||||
|
isValid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
} {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 기본적인 유효성 검사 로직
|
||||||
|
Object.entries(formData).forEach(([key, value]) => {
|
||||||
|
// 빈 값 체크 (null, undefined, 빈 문자열)
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
// 필수 필드는 향후 컴포넌트 설정에서 확인 가능
|
||||||
|
console.warn(`필드 '${key}'가 비어있습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 타입 검증
|
||||||
|
if (typeof value === "string" && value.trim() === "") {
|
||||||
|
console.warn(`필드 '${key}'가 공백만 포함되어 있습니다.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 최소한 하나의 필드는 있어야 함
|
||||||
|
if (Object.keys(formData).length === 0) {
|
||||||
|
errors.push("저장할 데이터가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 버튼 액션 설정들
|
||||||
|
*/
|
||||||
|
export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActionConfig>> = {
|
||||||
|
save: {
|
||||||
|
type: "save",
|
||||||
|
validateForm: true,
|
||||||
|
confirmMessage: "저장하시겠습니까?",
|
||||||
|
successMessage: "저장되었습니다.",
|
||||||
|
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
type: "submit",
|
||||||
|
validateForm: true,
|
||||||
|
successMessage: "제출되었습니다.",
|
||||||
|
errorMessage: "제출 중 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: "delete",
|
||||||
|
confirmMessage: "정말 삭제하시겠습니까?",
|
||||||
|
successMessage: "삭제되었습니다.",
|
||||||
|
errorMessage: "삭제 중 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
|
reset: {
|
||||||
|
type: "reset",
|
||||||
|
confirmMessage: "초기화하시겠습니까?",
|
||||||
|
successMessage: "초기화되었습니다.",
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
type: "cancel",
|
||||||
|
},
|
||||||
|
navigate: {
|
||||||
|
type: "navigate",
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
type: "modal",
|
||||||
|
modalSize: "md",
|
||||||
|
},
|
||||||
|
newWindow: {
|
||||||
|
type: "newWindow",
|
||||||
|
popupWidth: 800,
|
||||||
|
popupHeight: 600,
|
||||||
|
},
|
||||||
|
popup: {
|
||||||
|
type: "popup",
|
||||||
|
popupWidth: 600,
|
||||||
|
popupHeight: 400,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: "search",
|
||||||
|
successMessage: "검색을 실행했습니다.",
|
||||||
|
},
|
||||||
|
add: {
|
||||||
|
type: "add",
|
||||||
|
successMessage: "추가되었습니다.",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
type: "edit",
|
||||||
|
successMessage: "편집되었습니다.",
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
type: "close",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
/**
|
||||||
|
* 컴포넌트 ID로 해당 컴포넌트의 ConfigPanel을 동적으로 로드하는 유틸리티
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// 컴포넌트별 ConfigPanel 동적 import 맵
|
||||||
|
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
|
"text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"),
|
||||||
|
"number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"),
|
||||||
|
"date-input": () => import("@/lib/registry/components/date-input/DateInputConfigPanel"),
|
||||||
|
"textarea-basic": () => import("@/lib/registry/components/textarea-basic/TextareaBasicConfigPanel"),
|
||||||
|
"select-basic": () => import("@/lib/registry/components/select-basic/SelectBasicConfigPanel"),
|
||||||
|
"checkbox-basic": () => import("@/lib/registry/components/checkbox-basic/CheckboxBasicConfigPanel"),
|
||||||
|
"radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"),
|
||||||
|
"toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"),
|
||||||
|
"file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"),
|
||||||
|
"button-primary": () => import("@/lib/registry/components/button-primary/ButtonPrimaryConfigPanel"),
|
||||||
|
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
|
||||||
|
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
|
||||||
|
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
|
||||||
|
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
const configPanelCache = new Map<string, React.ComponentType<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 ID로 ConfigPanel 컴포넌트를 동적으로 로드
|
||||||
|
*/
|
||||||
|
export async function getComponentConfigPanel(componentId: string): Promise<React.ComponentType<any> | null> {
|
||||||
|
// 캐시에서 먼저 확인
|
||||||
|
if (configPanelCache.has(componentId)) {
|
||||||
|
return configPanelCache.get(componentId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 매핑에서 import 함수 찾기
|
||||||
|
const importFn = CONFIG_PANEL_MAP[componentId];
|
||||||
|
if (!importFn) {
|
||||||
|
console.warn(`컴포넌트 "${componentId}"에 대한 ConfigPanel을 찾을 수 없습니다.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔧 ConfigPanel 로드 중: ${componentId}`);
|
||||||
|
const module = await importFn();
|
||||||
|
|
||||||
|
// 모듈에서 ConfigPanel 컴포넌트 추출
|
||||||
|
const ConfigPanelComponent = module[`${toPascalCase(componentId)}ConfigPanel`] || module.default;
|
||||||
|
|
||||||
|
if (!ConfigPanelComponent) {
|
||||||
|
console.error(`컴포넌트 "${componentId}"의 ConfigPanel을 모듈에서 찾을 수 없습니다.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시에 저장
|
||||||
|
configPanelCache.set(componentId, ConfigPanelComponent);
|
||||||
|
console.log(`✅ ConfigPanel 로드 완료: ${componentId}`);
|
||||||
|
|
||||||
|
return ConfigPanelComponent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`컴포넌트 "${componentId}"의 ConfigPanel 로드 실패:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 ID가 ConfigPanel을 지원하는지 확인
|
||||||
|
*/
|
||||||
|
export function hasComponentConfigPanel(componentId: string): boolean {
|
||||||
|
return componentId in CONFIG_PANEL_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지원되는 모든 컴포넌트 ID 목록 조회
|
||||||
|
*/
|
||||||
|
export function getSupportedConfigPanelComponents(): string[] {
|
||||||
|
return Object.keys(CONFIG_PANEL_MAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kebab-case를 PascalCase로 변환
|
||||||
|
* text-input → TextInput
|
||||||
|
*/
|
||||||
|
function toPascalCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.split('-')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 설정 패널을 렌더링하는 React 컴포넌트
|
||||||
|
*/
|
||||||
|
export interface ComponentConfigPanelProps {
|
||||||
|
componentId: string;
|
||||||
|
config: Record<string, any>;
|
||||||
|
onChange: (config: Record<string, any>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> = ({
|
||||||
|
componentId,
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
async function loadConfigPanel() {
|
||||||
|
try {
|
||||||
|
console.log(`🔧 DynamicComponentConfigPanel: ${componentId} 로드 시작`);
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const component = await getComponentConfigPanel(componentId);
|
||||||
|
console.log(`🔧 DynamicComponentConfigPanel: ${componentId} 로드 결과:`, component);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setConfigPanelComponent(() => component);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ DynamicComponentConfigPanel: ${componentId} 로드 실패:`, err);
|
||||||
|
if (mounted) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfigPanel();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [componentId]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
|
<span className="text-sm font-medium">⏳ 로딩 중...</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">설정 패널을 불러오는 중입니다.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-dashed border-red-300 bg-red-50 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-red-600">
|
||||||
|
<span className="text-sm font-medium">⚠️ 로드 실패</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-red-500">설정 패널을 불러올 수 없습니다: {error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ConfigPanelComponent) {
|
||||||
|
console.warn(`⚠️ DynamicComponentConfigPanel: ${componentId} ConfigPanelComponent가 null`);
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-dashed border-yellow-300 bg-yellow-50 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-yellow-600">
|
||||||
|
<span className="text-sm font-medium">⚠️ 설정 패널 없음</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-yellow-500">컴포넌트 "{componentId}"에 대한 설정 패널이 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ConfigPanelComponent config={config} onChange={onChange} />;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* 웹타입을 새로운 컴포넌트 시스템의 컴포넌트 ID로 매핑하는 유틸리티
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WebTypeMapping {
|
||||||
|
webType: string;
|
||||||
|
componentId: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹타입 → 컴포넌트 ID 매핑 테이블
|
||||||
|
*/
|
||||||
|
export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
||||||
|
// 텍스트 입력
|
||||||
|
text: "text-input",
|
||||||
|
email: "text-input",
|
||||||
|
password: "text-input",
|
||||||
|
tel: "text-input",
|
||||||
|
|
||||||
|
// 숫자 입력
|
||||||
|
number: "number-input",
|
||||||
|
decimal: "number-input",
|
||||||
|
|
||||||
|
// 날짜/시간
|
||||||
|
date: "date-input",
|
||||||
|
datetime: "date-input",
|
||||||
|
time: "date-input",
|
||||||
|
|
||||||
|
// 텍스트 영역
|
||||||
|
textarea: "textarea-basic",
|
||||||
|
|
||||||
|
// 선택
|
||||||
|
select: "select-basic",
|
||||||
|
dropdown: "select-basic",
|
||||||
|
|
||||||
|
// 체크박스/라디오
|
||||||
|
checkbox: "checkbox-basic",
|
||||||
|
radio: "radio-basic",
|
||||||
|
boolean: "toggle-switch",
|
||||||
|
|
||||||
|
// 파일
|
||||||
|
file: "file-upload",
|
||||||
|
|
||||||
|
// 버튼
|
||||||
|
button: "button-primary",
|
||||||
|
|
||||||
|
// 기타
|
||||||
|
label: "text-display",
|
||||||
|
code: "text-input", // 임시로 텍스트 입력 사용
|
||||||
|
entity: "select-basic", // 임시로 선택상자 사용
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹타입을 컴포넌트 ID로 변환
|
||||||
|
*/
|
||||||
|
export function getComponentIdFromWebType(webType: string): string {
|
||||||
|
const componentId = WEB_TYPE_COMPONENT_MAPPING[webType];
|
||||||
|
|
||||||
|
if (!componentId) {
|
||||||
|
console.warn(`웹타입 "${webType}"에 대한 컴포넌트 매핑을 찾을 수 없습니다. 기본값 'text-input' 사용`);
|
||||||
|
return "text-input"; // 기본값
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`웹타입 "${webType}" → 컴포넌트 "${componentId}" 매핑`);
|
||||||
|
return componentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 ID를 웹타입으로 역변환 (필요한 경우)
|
||||||
|
*/
|
||||||
|
export function getWebTypeFromComponentId(componentId: string): string {
|
||||||
|
const entry = Object.entries(WEB_TYPE_COMPONENT_MAPPING).find(([_, id]) => id === componentId);
|
||||||
|
return entry ? entry[0] : "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지원되는 모든 웹타입 목록 조회
|
||||||
|
*/
|
||||||
|
export function getSupportedWebTypes(): string[] {
|
||||||
|
return Object.keys(WEB_TYPE_COMPONENT_MAPPING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지원되는 모든 컴포넌트 ID 목록 조회
|
||||||
|
*/
|
||||||
|
export function getSupportedComponentIds(): string[] {
|
||||||
|
return [...new Set(Object.values(WEB_TYPE_COMPONENT_MAPPING))];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 매핑 정보 조회
|
||||||
|
*/
|
||||||
|
export function getWebTypeMappings(): WebTypeMapping[] {
|
||||||
|
return Object.entries(WEB_TYPE_COMPONENT_MAPPING).map(([webType, componentId]) => ({
|
||||||
|
webType,
|
||||||
|
componentId,
|
||||||
|
description: `${webType} → ${componentId}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
|
|
@ -884,9 +885,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.30",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1113,9 +1114,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.1.tgz",
|
||||||
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
|
"integrity": "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1126,53 +1127,53 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.1.tgz",
|
||||||
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
|
"integrity": "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.1.tgz",
|
||||||
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
|
"integrity": "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0",
|
"@prisma/debug": "6.16.1",
|
||||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"@prisma/fetch-engine": "6.15.0",
|
"@prisma/fetch-engine": "6.16.1",
|
||||||
"@prisma/get-platform": "6.15.0"
|
"@prisma/get-platform": "6.16.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz",
|
||||||
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
|
"integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.1.tgz",
|
||||||
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
|
"integrity": "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0",
|
"@prisma/debug": "6.16.1",
|
||||||
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
|
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"@prisma/get-platform": "6.15.0"
|
"@prisma/get-platform": "6.16.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.1.tgz",
|
||||||
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
|
"integrity": "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.15.0"
|
"@prisma/debug": "6.16.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
|
|
@ -2511,9 +2512,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.87.1",
|
"version": "5.87.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.87.4.tgz",
|
||||||
"integrity": "sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA==",
|
"integrity": "sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -2521,9 +2522,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/query-devtools": {
|
"node_modules/@tanstack/query-devtools": {
|
||||||
"version": "5.86.0",
|
"version": "5.87.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.86.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.87.3.tgz",
|
||||||
"integrity": "sha512-/JDw9BP80eambEK/EsDMGAcsL2VFT+8F5KCOwierjPU7QP8Wt1GT32yJpn3qOinBM8/zS3Jy36+F0GiyJp411A==",
|
"integrity": "sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|
@ -2532,12 +2533,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query": {
|
"node_modules/@tanstack/react-query": {
|
||||||
"version": "5.87.1",
|
"version": "5.87.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.87.4.tgz",
|
||||||
"integrity": "sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg==",
|
"integrity": "sha512-T5GT/1ZaNsUXf5I3RhcYuT17I4CPlbZgyLxc/ZGv7ciS6esytlbjb3DgUFO6c8JWYMDpdjSWInyGZUErgzqhcA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.87.1"
|
"@tanstack/query-core": "5.87.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -2548,20 +2549,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-query-devtools": {
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
"version": "5.87.1",
|
"version": "5.87.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.87.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.87.4.tgz",
|
||||||
"integrity": "sha512-YPuEub8RQrrsXOxoiMJn33VcGPIeuVINWBgLu9RLSQB8ueXaKlGLZ3NJkahGpbt2AbWf749FQ6R+1jBFk3kdCA==",
|
"integrity": "sha512-JYcnVJBBW1DCPyNGM0S2CyrLpe6KFiL2gpYd/k9tAp62Du7+Y27zkzd+dKFyxpFadYaTxsx4kUA7YvnkMLVUoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-devtools": "5.86.0"
|
"@tanstack/query-devtools": "5.87.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tanstack/react-query": "^5.87.1",
|
"@tanstack/react-query": "^5.87.4",
|
||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2599,9 +2600,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|
@ -2669,17 +2670,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
|
||||||
"integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==",
|
"integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.42.0",
|
"@typescript-eslint/scope-manager": "8.43.0",
|
||||||
"@typescript-eslint/type-utils": "8.42.0",
|
"@typescript-eslint/type-utils": "8.43.0",
|
||||||
"@typescript-eslint/utils": "8.42.0",
|
"@typescript-eslint/utils": "8.43.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
|
|
@ -2693,7 +2694,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.42.0",
|
"@typescript-eslint/parser": "^8.43.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2709,16 +2710,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
|
||||||
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
|
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.42.0",
|
"@typescript-eslint/scope-manager": "8.43.0",
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
"@typescript-eslint/typescript-estree": "8.43.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2734,14 +2735,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
|
||||||
"integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==",
|
"integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.42.0",
|
"@typescript-eslint/tsconfig-utils": "^8.43.0",
|
||||||
"@typescript-eslint/types": "^8.42.0",
|
"@typescript-eslint/types": "^8.43.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2756,14 +2757,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
|
||||||
"integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==",
|
"integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.42.0"
|
"@typescript-eslint/visitor-keys": "8.43.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2774,9 +2775,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
|
||||||
"integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==",
|
"integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2791,15 +2792,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
|
||||||
"integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==",
|
"integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.42.0",
|
"@typescript-eslint/typescript-estree": "8.43.0",
|
||||||
"@typescript-eslint/utils": "8.42.0",
|
"@typescript-eslint/utils": "8.43.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2816,9 +2817,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
|
||||||
"integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==",
|
"integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2830,16 +2831,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
|
||||||
"integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==",
|
"integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.42.0",
|
"@typescript-eslint/project-service": "8.43.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.42.0",
|
"@typescript-eslint/tsconfig-utils": "8.43.0",
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.42.0",
|
"@typescript-eslint/visitor-keys": "8.43.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
|
|
@ -2915,16 +2916,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
|
||||||
"integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==",
|
"integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.42.0",
|
"@typescript-eslint/scope-manager": "8.43.0",
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.42.0"
|
"@typescript-eslint/typescript-estree": "8.43.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2939,13 +2940,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.42.0",
|
"version": "8.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
|
||||||
"integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==",
|
"integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.42.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -3520,9 +3521,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.11.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
|
||||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
|
|
@ -3759,6 +3760,22 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cmdk": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-id": "^1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "^2.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
|
@ -6199,9 +6216,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.18",
|
"version": "0.30.19",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||||
"integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
|
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -6928,15 +6945,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.15.0",
|
"version": "6.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz",
|
||||||
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
|
"integrity": "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.15.0",
|
"@prisma/config": "6.16.1",
|
||||||
"@prisma/engines": "6.15.0"
|
"@prisma/engines": "6.16.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
|
|
@ -8350,9 +8367,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
|
||||||
"integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==",
|
"integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.4.4",
|
"next": "15.4.4",
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,15 @@ export interface ComponentRendererProps {
|
||||||
component: any; // ComponentData from screen.ts
|
component: any; // ComponentData from screen.ts
|
||||||
isDesignMode?: boolean;
|
isDesignMode?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
isInteractive?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onDragStart?: (e: React.DragEvent) => void;
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
onDragEnd?: (e: React.DragEvent) => void;
|
onDragEnd?: (e: React.DragEvent) => void;
|
||||||
onUpdate?: (updates: Partial<any>) => void;
|
onUpdate?: (updates: Partial<any>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
formData?: Record<string, any>;
|
||||||
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,10 @@ export type ButtonActionType =
|
||||||
| "reset" // 초기화
|
| "reset" // 초기화
|
||||||
| "submit" // 제출
|
| "submit" // 제출
|
||||||
| "close" // 닫기
|
| "close" // 닫기
|
||||||
| "popup" // 모달 열기
|
| "popup" // 팝업 열기
|
||||||
| "navigate" // 페이지 이동
|
| "modal" // 모달 열기
|
||||||
| "custom"; // 사용자 정의
|
| "newWindow" // 새 창 열기
|
||||||
|
| "navigate"; // 페이지 이동
|
||||||
|
|
||||||
// 위치 정보
|
// 위치 정보
|
||||||
export interface Position {
|
export interface Position {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue