Compare commits
7 Commits
d668814e03
...
ee77a46168
| Author | SHA1 | Date |
|---|---|---|
|
|
ee77a46168 | |
|
|
8179946cd8 | |
|
|
3172f772ba | |
|
|
70d2c96c80 | |
|
|
b104cd94f2 | |
|
|
23f7b89cc5 | |
|
|
4996dd5562 |
|
|
@ -21,9 +21,15 @@ export default function ScreenViewPage() {
|
||||||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
|
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
|
||||||
|
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 테이블 새로고침을 위한 키 (값이 변경되면 테이블이 리렌더링됨)
|
||||||
|
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
||||||
|
|
||||||
// 편집 모달 상태
|
// 편집 모달 상태
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [editModalConfig, setEditModalConfig] = useState<{
|
const [editModalConfig, setEditModalConfig] = useState<{
|
||||||
|
|
@ -172,6 +178,24 @@ export default function ScreenViewPage() {
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
isDesignMode={false}
|
isDesignMode={false}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
|
screenId={screenId}
|
||||||
|
tableName={screen?.tableName}
|
||||||
|
selectedRowsData={selectedRowsData}
|
||||||
|
onSelectedRowsChange={(_, selectedData) => {
|
||||||
|
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
||||||
|
setSelectedRowsData(selectedData);
|
||||||
|
}}
|
||||||
|
refreshKey={tableRefreshKey}
|
||||||
|
onRefresh={() => {
|
||||||
|
console.log("🔄 테이블 새로고침 요청됨");
|
||||||
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
|
setSelectedRowsData([]); // 선택 해제
|
||||||
|
}}
|
||||||
|
formData={formData}
|
||||||
|
onFormDataChange={(fieldName, value) => {
|
||||||
|
console.log("📝 폼 데이터 변경:", fieldName, "=", value);
|
||||||
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 자식 컴포넌트들 */}
|
{/* 자식 컴포넌트들 */}
|
||||||
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
{(component.type === "group" || component.type === "container" || component.type === "area") &&
|
||||||
|
|
@ -195,6 +219,24 @@ export default function ScreenViewPage() {
|
||||||
isSelected={false}
|
isSelected={false}
|
||||||
isDesignMode={false}
|
isDesignMode={false}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
|
screenId={screenId}
|
||||||
|
tableName={screen?.tableName}
|
||||||
|
selectedRowsData={selectedRowsData}
|
||||||
|
onSelectedRowsChange={(_, selectedData) => {
|
||||||
|
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
|
||||||
|
setSelectedRowsData(selectedData);
|
||||||
|
}}
|
||||||
|
refreshKey={tableRefreshKey}
|
||||||
|
onRefresh={() => {
|
||||||
|
console.log("🔄 테이블 새로고침 요청됨 (자식)");
|
||||||
|
setTableRefreshKey((prev) => prev + 1);
|
||||||
|
setSelectedRowsData([]); // 선택 해제
|
||||||
|
}}
|
||||||
|
formData={formData}
|
||||||
|
onFormDataChange={(fieldName, value) => {
|
||||||
|
console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value);
|
||||||
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
|
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
|
||||||
import { companyAPI } from "@/lib/api/company";
|
import { companyAPI } from "@/lib/api/company";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi, menuScreenApi } from "@/lib/api/screen";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
@ -598,6 +598,48 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
// 화면 할당이 있는 경우 추가 처리
|
||||||
|
if (urlType === "screen" && selectedScreen) {
|
||||||
|
try {
|
||||||
|
// menuId는 response에서 반환되거나 기존 menuId 사용
|
||||||
|
const targetMenuId = menuId || response.data?.objid;
|
||||||
|
const menuObjid = parseInt(targetMenuId?.toString() || "0");
|
||||||
|
|
||||||
|
if (menuObjid > 0) {
|
||||||
|
console.log("📋 화면-메뉴 관계 테이블 업데이트 시작:", {
|
||||||
|
screenId: selectedScreen.screenId,
|
||||||
|
menuObjid,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. 기존 할당된 화면들 먼저 조회
|
||||||
|
try {
|
||||||
|
const existingScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||||
|
console.log("📋 기존 할당된 화면:", existingScreens.length, "개");
|
||||||
|
|
||||||
|
// 2. 기존 화면들 모두 제거
|
||||||
|
for (const existingScreen of existingScreens) {
|
||||||
|
try {
|
||||||
|
await menuScreenApi.unassignScreenFromMenu(existingScreen.screenId, menuObjid);
|
||||||
|
console.log(`✅ 기존 화면 제거 완료: ${existingScreen.screenName}`);
|
||||||
|
} catch (unassignError) {
|
||||||
|
console.warn(`⚠️ 기존 화면 제거 실패: ${existingScreen.screenName}`, unassignError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (getError) {
|
||||||
|
console.warn("⚠️ 기존 화면 조회 실패 (계속 진행):", getError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 새 화면 할당
|
||||||
|
await menuScreenApi.assignScreenToMenu(selectedScreen.screenId, menuObjid);
|
||||||
|
console.log("✅ 새 화면 할당 완료");
|
||||||
|
}
|
||||||
|
} catch (assignError) {
|
||||||
|
console.error("❌ 화면-메뉴 관계 테이블 할당 실패:", assignError);
|
||||||
|
// 할당 실패는 경고만 하고 메뉴 저장은 성공으로 처리
|
||||||
|
toast.warning("메뉴는 저장되었으나 화면 할당에 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(response.message);
|
toast.success(response.message);
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||||
|
|
||||||
|
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
|
||||||
|
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
|
||||||
|
|
||||||
// 팝업 화면 상태
|
// 팝업 화면 상태
|
||||||
const [popupScreen, setPopupScreen] = useState<{
|
const [popupScreen, setPopupScreen] = useState<{
|
||||||
screenId: number;
|
screenId: number;
|
||||||
|
|
@ -186,6 +189,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
onFormDataChange={handleFormDataChange}
|
onFormDataChange={handleFormDataChange}
|
||||||
screenId={screenInfo?.id}
|
screenId={screenInfo?.id}
|
||||||
tableName={screenInfo?.tableName}
|
tableName={screenInfo?.tableName}
|
||||||
|
selectedRowsData={selectedRowsData}
|
||||||
|
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||||
|
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
|
||||||
|
setSelectedRowsData(selectedData);
|
||||||
|
}}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
console.log("🔄 버튼에서 테이블 새로고침 요청됨");
|
console.log("🔄 버튼에서 테이블 새로고침 요청됨");
|
||||||
// 테이블 컴포넌트는 자체적으로 loadData 호출
|
// 테이블 컴포넌트는 자체적으로 loadData 호출
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,66 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { type, id, position, size, style = {} } = component;
|
const { type, id, position, size, style = {} } = component;
|
||||||
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
|
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
|
||||||
|
const [actualHeight, setActualHeight] = useState<number | null>(null);
|
||||||
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 플로우 위젯의 실제 높이 측정
|
||||||
|
useEffect(() => {
|
||||||
|
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||||
|
|
||||||
|
if (isFlowWidget && contentRef.current) {
|
||||||
|
const measureHeight = () => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
// getBoundingClientRect()로 실제 렌더링된 높이 측정
|
||||||
|
const rect = contentRef.current.getBoundingClientRect();
|
||||||
|
const measured = rect.height;
|
||||||
|
|
||||||
|
// scrollHeight도 함께 확인하여 더 큰 값 사용
|
||||||
|
const scrollHeight = contentRef.current.scrollHeight;
|
||||||
|
const rawHeight = Math.max(measured, scrollHeight);
|
||||||
|
|
||||||
|
// 40px 단위로 올림
|
||||||
|
const finalHeight = Math.ceil(rawHeight / 40) * 40;
|
||||||
|
|
||||||
|
if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) {
|
||||||
|
setActualHeight(finalHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 초기 측정 (렌더링 완료 후)
|
||||||
|
const initialTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 추가 측정 (데이터 로딩 완료 대기)
|
||||||
|
const delayedTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 스텝 클릭 등으로 높이가 변경될 때를 위한 추가 측정
|
||||||
|
const extendedTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// ResizeObserver로 크기 변화 감지 (스텝 클릭 시 데이터 테이블 펼쳐짐)
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
// 약간의 지연을 두고 측정 (DOM 업데이트 완료 대기)
|
||||||
|
setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(contentRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(initialTimer);
|
||||||
|
clearTimeout(delayedTimer);
|
||||||
|
clearTimeout(extendedTimer);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [type, id]);
|
||||||
|
|
||||||
// 전역 파일 상태 변경 감지 (해당 컴포넌트만)
|
// 전역 파일 상태 변경 감지 (해당 컴포넌트만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -314,12 +374,20 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
}, [component.id, fileUpdateTrigger]);
|
}, [component.id, fileUpdateTrigger]);
|
||||||
|
|
||||||
// 컴포넌트 스타일 계산
|
// 컴포넌트 스타일 계산
|
||||||
|
const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget");
|
||||||
|
|
||||||
|
// 높이 결정 로직
|
||||||
|
let finalHeight = size?.height || 40;
|
||||||
|
if (isFlowWidget && actualHeight) {
|
||||||
|
finalHeight = actualHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const componentStyle = {
|
const componentStyle = {
|
||||||
position: "absolute" as const,
|
position: "absolute" as const,
|
||||||
left: position?.x || 0,
|
left: position?.x || 0,
|
||||||
top: position?.y || 0,
|
top: position?.y || 0,
|
||||||
width: size?.width || 200,
|
width: size?.width || 200,
|
||||||
height: size?.height || 40,
|
height: finalHeight,
|
||||||
zIndex: position?.z || 1,
|
zIndex: position?.z || 1,
|
||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
|
|
@ -358,7 +426,10 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
{/* 컴포넌트 타입별 렌더링 */}
|
{/* 컴포넌트 타입별 렌더링 */}
|
||||||
<div className="h-full w-full">
|
<div
|
||||||
|
ref={isFlowWidget ? contentRef : undefined}
|
||||||
|
className={isFlowWidget ? "h-auto w-full" : "h-full w-full"}
|
||||||
|
>
|
||||||
{/* 영역 타입 */}
|
{/* 영역 타입 */}
|
||||||
{type === "area" && renderArea(component, children)}
|
{type === "area" && renderArea(component, children)}
|
||||||
|
|
||||||
|
|
@ -422,7 +493,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
console.log("🔍 RealtimePreview 최종 flowComponent:", flowComponent);
|
console.log("🔍 RealtimePreview 최종 flowComponent:", flowComponent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="h-auto w-full">
|
||||||
<FlowWidget component={flowComponent as any} />
|
<FlowWidget component={flowComponent as any} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@ interface RealtimePreviewProps {
|
||||||
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; // 존 클릭 핸들러
|
||||||
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
|
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
|
||||||
|
|
||||||
|
// 버튼 액션을 위한 props
|
||||||
|
screenId?: number;
|
||||||
|
tableName?: string;
|
||||||
|
selectedRowsData?: any[];
|
||||||
|
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
|
||||||
|
refreshKey?: number;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
|
||||||
|
// 폼 데이터 관련 props
|
||||||
|
formData?: Record<string, any>;
|
||||||
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
|
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
|
||||||
|
|
@ -77,14 +89,101 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onZoneComponentDrop,
|
onZoneComponentDrop,
|
||||||
onZoneClick,
|
onZoneClick,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
selectedRowsData,
|
||||||
|
onSelectedRowsChange,
|
||||||
|
refreshKey,
|
||||||
|
onRefresh,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [actualHeight, setActualHeight] = React.useState<number | null>(null);
|
||||||
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const lastUpdatedHeight = React.useRef<number | null>(null);
|
||||||
|
|
||||||
|
// 플로우 위젯의 실제 높이 측정
|
||||||
|
React.useEffect(() => {
|
||||||
|
const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget";
|
||||||
|
|
||||||
|
if (isFlowWidget && contentRef.current) {
|
||||||
|
const measureHeight = () => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
// getBoundingClientRect()로 실제 렌더링된 높이 측정
|
||||||
|
const rect = contentRef.current.getBoundingClientRect();
|
||||||
|
const measured = rect.height;
|
||||||
|
|
||||||
|
// scrollHeight도 함께 확인하여 더 큰 값 사용
|
||||||
|
const scrollHeight = contentRef.current.scrollHeight;
|
||||||
|
const rawHeight = Math.max(measured, scrollHeight);
|
||||||
|
|
||||||
|
// 40px 단위로 올림
|
||||||
|
const finalHeight = Math.ceil(rawHeight / 40) * 40;
|
||||||
|
|
||||||
|
if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) {
|
||||||
|
setActualHeight(finalHeight);
|
||||||
|
|
||||||
|
// 컴포넌트의 실제 size.height도 업데이트 (중복 업데이트 방지)
|
||||||
|
if (onConfigChange && finalHeight !== lastUpdatedHeight.current && finalHeight !== component.size?.height) {
|
||||||
|
lastUpdatedHeight.current = finalHeight;
|
||||||
|
console.log("🔄 플로우 위젯 높이 업데이트 이벤트 발송:", {
|
||||||
|
componentId: component.id,
|
||||||
|
oldHeight: component.size?.height,
|
||||||
|
newHeight: finalHeight,
|
||||||
|
});
|
||||||
|
// size는 별도 속성이므로 직접 업데이트
|
||||||
|
const event = new CustomEvent('updateComponentSize', {
|
||||||
|
detail: {
|
||||||
|
componentId: component.id,
|
||||||
|
height: finalHeight
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 초기 측정 (렌더링 완료 후)
|
||||||
|
const initialTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 추가 측정 (데이터 로딩 완료 대기)
|
||||||
|
const delayedTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 스텝 클릭 등으로 높이가 변경될 때를 위한 추가 측정
|
||||||
|
const extendedTimer = setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// ResizeObserver로 크기 변화 감지 (스텝 클릭 시 데이터 테이블 펼쳐짐)
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
// 약간의 지연을 두고 측정 (DOM 업데이트 완료 대기)
|
||||||
|
setTimeout(() => {
|
||||||
|
measureHeight();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(contentRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(initialTimer);
|
||||||
|
clearTimeout(delayedTimer);
|
||||||
|
clearTimeout(extendedTimer);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [component.type, component.id, actualHeight, component.size?.height, onConfigChange]);
|
||||||
const { id, type, position, size, style: componentStyle } = component;
|
const { id, type, position, size, style: componentStyle } = component;
|
||||||
|
|
||||||
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
|
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
|
||||||
const selectionStyle = isSelected
|
const selectionStyle = isSelected
|
||||||
? {
|
? {
|
||||||
outline: "2px solid rgb(59, 130, 246)",
|
outline: "2px solid rgb(59, 130, 246)",
|
||||||
outlineOffset: "2px",
|
outlineOffset: "0px", // 스크롤 방지를 위해 0으로 설정
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
@ -106,6 +205,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHeight = () => {
|
const getHeight = () => {
|
||||||
|
// 플로우 위젯의 경우 측정된 높이 사용
|
||||||
|
const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget";
|
||||||
|
if (isFlowWidget && actualHeight) {
|
||||||
|
return `${actualHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
// 1순위: style.height가 있으면 우선 사용
|
// 1순위: style.height가 있으면 우선 사용
|
||||||
if (componentStyle?.height) {
|
if (componentStyle?.height) {
|
||||||
return componentStyle.height;
|
return componentStyle.height;
|
||||||
|
|
@ -161,7 +266,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
>
|
>
|
||||||
{/* 동적 컴포넌트 렌더링 */}
|
{/* 동적 컴포넌트 렌더링 */}
|
||||||
<div
|
<div
|
||||||
className={`h-full w-full max-w-full ${
|
ref={component.type === "component" && (component as any).componentType === "flow-widget" ? contentRef : undefined}
|
||||||
|
className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} w-full max-w-full ${
|
||||||
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-visible"
|
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-visible"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -178,6 +284,14 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
onZoneComponentDrop={onZoneComponentDrop}
|
onZoneComponentDrop={onZoneComponentDrop}
|
||||||
onZoneClick={onZoneClick}
|
onZoneClick={onZoneClick}
|
||||||
onConfigChange={onConfigChange}
|
onConfigChange={onConfigChange}
|
||||||
|
screenId={screenId}
|
||||||
|
tableName={tableName}
|
||||||
|
selectedRowsData={selectedRowsData}
|
||||||
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
|
refreshKey={refreshKey}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
formData={formData}
|
||||||
|
onFormDataChange={onFormDataChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2906,9 +2906,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const relativeMouseX = event.clientX - rect.left;
|
const relativeMouseX = event.clientX - rect.left;
|
||||||
const relativeMouseY = event.clientY - rect.top;
|
const relativeMouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
// 컴포넌트 크기 가져오기
|
||||||
|
const draggedComp = layout.components.find((c) => c.id === dragState.draggedComponent.id);
|
||||||
|
const componentWidth = draggedComp?.size?.width || 100;
|
||||||
|
const componentHeight = draggedComp?.size?.height || 40;
|
||||||
|
|
||||||
|
// 경계 제한 적용
|
||||||
|
const rawX = relativeMouseX - dragState.grabOffset.x;
|
||||||
|
const rawY = relativeMouseY - dragState.grabOffset.y;
|
||||||
|
|
||||||
const newPosition = {
|
const newPosition = {
|
||||||
x: relativeMouseX - dragState.grabOffset.x,
|
x: Math.max(0, Math.min(rawX, screenResolution.width - componentWidth)),
|
||||||
y: relativeMouseY - dragState.grabOffset.y,
|
y: Math.max(0, Math.min(rawY, screenResolution.height - componentHeight)),
|
||||||
z: (dragState.draggedComponent.position as Position).z || 1,
|
z: (dragState.draggedComponent.position as Position).z || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3002,6 +3011,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
z: originalComponent.position.z || 1,
|
z: originalComponent.position.z || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 캔버스 경계 제한 (컴포넌트가 화면 밖으로 나가지 않도록)
|
||||||
|
const componentWidth = comp.size?.width || 100;
|
||||||
|
const componentHeight = comp.size?.height || 40;
|
||||||
|
|
||||||
|
// 최소 위치: 0, 최대 위치: 캔버스 크기 - 컴포넌트 크기
|
||||||
|
newPosition.x = Math.max(0, Math.min(newPosition.x, screenResolution.width - componentWidth));
|
||||||
|
newPosition.y = Math.max(0, Math.min(newPosition.y, screenResolution.height - componentHeight));
|
||||||
|
|
||||||
// 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용
|
// 그룹 내부 컴포넌트인 경우 패딩을 고려한 격자 스냅 적용
|
||||||
if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) {
|
if (comp.parentId && layout.gridSettings?.snapToGrid && gridInfo) {
|
||||||
const { columnWidth } = gridInfo;
|
const { columnWidth } = gridInfo;
|
||||||
|
|
@ -3895,6 +3912,73 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
selectedScreen,
|
selectedScreen,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 플로우 위젯 높이 자동 업데이트 이벤트 리스너
|
||||||
|
useEffect(() => {
|
||||||
|
const handleComponentSizeUpdate = (event: CustomEvent) => {
|
||||||
|
const { componentId, height } = event.detail;
|
||||||
|
|
||||||
|
console.log("📥 ScreenDesigner에서 높이 업데이트 이벤트 수신:", {
|
||||||
|
componentId,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 해당 컴포넌트 찾기
|
||||||
|
const targetComponent = layout.components.find((c) => c.id === componentId);
|
||||||
|
if (!targetComponent) {
|
||||||
|
console.log("⚠️ 컴포넌트를 찾을 수 없음:", componentId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 같은 높이면 업데이트 안함
|
||||||
|
if (targetComponent.size?.height === height) {
|
||||||
|
console.log("ℹ️ 이미 같은 높이:", height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 컴포넌트 높이 업데이트 중:", {
|
||||||
|
componentId,
|
||||||
|
oldHeight: targetComponent.size?.height,
|
||||||
|
newHeight: height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트 높이 업데이트
|
||||||
|
const updatedComponents = layout.components.map((comp) => {
|
||||||
|
if (comp.id === componentId) {
|
||||||
|
return {
|
||||||
|
...comp,
|
||||||
|
size: {
|
||||||
|
...comp.size,
|
||||||
|
width: comp.size?.width || 100,
|
||||||
|
height: height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return comp;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newLayout = {
|
||||||
|
...layout,
|
||||||
|
components: updatedComponents,
|
||||||
|
};
|
||||||
|
|
||||||
|
setLayout(newLayout);
|
||||||
|
|
||||||
|
// 선택된 컴포넌트도 업데이트
|
||||||
|
if (selectedComponent?.id === componentId) {
|
||||||
|
const updatedComponent = updatedComponents.find((c) => c.id === componentId);
|
||||||
|
if (updatedComponent) {
|
||||||
|
setSelectedComponent(updatedComponent);
|
||||||
|
console.log("✅ 선택된 컴포넌트도 업데이트됨");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("updateComponentSize", handleComponentSizeUpdate as EventListener);
|
||||||
|
};
|
||||||
|
}, [layout, selectedComponent]);
|
||||||
|
|
||||||
if (!selectedScreen) {
|
if (!selectedScreen) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-full items-center justify-center">
|
<div className="bg-background flex h-full items-center justify-center">
|
||||||
|
|
@ -4007,20 +4091,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
minHeight: Math.max(screenResolution.height, 800) * zoomLevel,
|
minHeight: Math.max(screenResolution.height, 800) * zoomLevel,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */}
|
{/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
|
||||||
<div
|
<div
|
||||||
className="bg-background border-border border shadow-lg"
|
className="bg-background border-border border shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
width: screenResolution.width,
|
width: `${screenResolution.width}px`,
|
||||||
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
|
height: `${Math.max(screenResolution.height, 800)}px`,
|
||||||
minHeight: screenResolution.height,
|
minWidth: `${screenResolution.width}px`,
|
||||||
transform: `scale(${zoomLevel})`, // 줌 레벨에 따라 시각적으로 확대/축소
|
maxWidth: `${screenResolution.width}px`,
|
||||||
|
minHeight: `${screenResolution.height}px`,
|
||||||
|
flexShrink: 0,
|
||||||
|
transform: `scale(${zoomLevel})`,
|
||||||
transformOrigin: "top center",
|
transformOrigin: "top center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="bg-background relative h-full w-full overflow-auto"
|
className="bg-background relative h-full w-full overflow-visible"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) {
|
if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) {
|
||||||
setSelectedComponent(null);
|
setSelectedComponent(null);
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,18 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [component.id]);
|
}, [component.id]);
|
||||||
|
|
||||||
// 화면 목록 가져오기
|
// 화면 목록 가져오기 (전체 목록)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchScreens = async () => {
|
const fetchScreens = async () => {
|
||||||
try {
|
try {
|
||||||
setScreensLoading(true);
|
setScreensLoading(true);
|
||||||
const response = await apiClient.get("/screen-management/screens");
|
// 전체 목록을 가져오기 위해 size를 큰 값으로 설정
|
||||||
|
const response = await apiClient.get("/screen-management/screens", {
|
||||||
|
params: {
|
||||||
|
page: 1,
|
||||||
|
size: 9999, // 매우 큰 값으로 설정하여 전체 목록 가져오기
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.success && Array.isArray(response.data.data)) {
|
if (response.data.success && Array.isArray(response.data.data)) {
|
||||||
const screenList = response.data.data.map((screen: any) => ({
|
const screenList = response.data.data.map((screen: any) => ({
|
||||||
|
|
@ -194,17 +200,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="save">저장</SelectItem>
|
<SelectItem value="save">저장</SelectItem>
|
||||||
<SelectItem value="cancel">취소</SelectItem>
|
|
||||||
<SelectItem value="delete">삭제</SelectItem>
|
<SelectItem value="delete">삭제</SelectItem>
|
||||||
<SelectItem value="edit">수정</SelectItem>
|
<SelectItem value="edit">편집</SelectItem>
|
||||||
<SelectItem value="add">추가</SelectItem>
|
|
||||||
<SelectItem value="search">검색</SelectItem>
|
|
||||||
<SelectItem value="reset">초기화</SelectItem>
|
|
||||||
<SelectItem value="submit">제출</SelectItem>
|
|
||||||
<SelectItem value="close">닫기</SelectItem>
|
|
||||||
<SelectItem value="modal">모달 열기</SelectItem>
|
|
||||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||||
<SelectItem value="control">제어 (조건 체크만)</SelectItem>
|
<SelectItem value="modal">모달 열기</SelectItem>
|
||||||
|
<SelectItem value="control">제어 흐름</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ComponentData,
|
ComponentData,
|
||||||
|
|
@ -96,14 +95,36 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}
|
}
|
||||||
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
||||||
|
|
||||||
// 컴포넌트가 선택되지 않았을 때
|
// 컴포넌트가 선택되지 않았을 때도 해상도 설정은 표시
|
||||||
if (!selectedComponent) {
|
if (!selectedComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center p-4 text-center">
|
<div className="flex h-full flex-col bg-white">
|
||||||
|
{/* 해상도 설정만 표시 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
|
<div className="space-y-4 text-xs">
|
||||||
|
{currentResolution && onResolutionChange && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Monitor className="h-3 w-3 text-primary" />
|
||||||
|
<h4 className="text-xs font-semibold">해상도 설정</h4>
|
||||||
|
</div>
|
||||||
|
<ResolutionPanel
|
||||||
|
currentResolution={currentResolution}
|
||||||
|
onResolutionChange={onResolutionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 안내 메시지 */}
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<Settings className="mb-2 h-8 w-8 text-gray-300" />
|
<Settings className="mb-2 h-8 w-8 text-gray-300" />
|
||||||
<p className="text-[10px] text-gray-500">컴포넌트를 선택하여</p>
|
<p className="text-[10px] text-gray-500">컴포넌트를 선택하여</p>
|
||||||
<p className="text-[10px] text-gray-500">속성을 편집하세요</p>
|
<p className="text-[10px] text-gray-500">속성을 편집하세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,26 +361,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
{/* 옵션 */}
|
{/* 옵션 */}
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={widget.visible !== false}
|
|
||||||
onCheckedChange={(checked) => handleUpdate("visible", checked)}
|
|
||||||
/>
|
|
||||||
<Label>표시</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={widget.disabled === true}
|
|
||||||
onCheckedChange={(checked) => handleUpdate("disabled", checked)}
|
|
||||||
/>
|
|
||||||
<Label>비활성화</Label>
|
|
||||||
</div>
|
|
||||||
{widget.required !== undefined && (
|
{widget.required !== undefined && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={widget.required === true}
|
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
|
||||||
onCheckedChange={(checked) => handleUpdate("required", checked)}
|
onCheckedChange={(checked) => handleUpdate("componentConfig.required", checked)}
|
||||||
/>
|
/>
|
||||||
<Label>필수 입력</Label>
|
<Label>필수 입력</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -367,8 +374,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
{widget.readonly !== undefined && (
|
{widget.readonly !== undefined && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={widget.readonly === true}
|
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
|
||||||
onCheckedChange={(checked) => handleUpdate("readonly", checked)}
|
onCheckedChange={(checked) => handleUpdate("componentConfig.readonly", checked)}
|
||||||
/>
|
/>
|
||||||
<Label>읽기 전용</Label>
|
<Label>읽기 전용</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -524,8 +531,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
tables={tables}
|
tables={tables}
|
||||||
onChange={(newConfig) => {
|
onChange={(newConfig) => {
|
||||||
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
||||||
// 전체 componentConfig를 업데이트
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
handleUpdate("componentConfig", newConfig);
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
|
handleUpdate(`componentConfig.${key}`, value);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -603,47 +612,39 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 탭 컨텐츠 */}
|
{/* 통합 컨텐츠 (탭 제거) */}
|
||||||
<Tabs defaultValue="properties" className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
<TabsList className="grid h-7 w-full flex-shrink-0 grid-cols-2">
|
<div className="space-y-4 text-xs">
|
||||||
<TabsTrigger value="properties" className="text-[10px]">
|
{/* 해상도 설정 - 항상 맨 위에 표시 */}
|
||||||
편집
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="styles" className="text-[10px]">
|
|
||||||
<Palette className="mr-0.5 h-2.5 w-2.5" />
|
|
||||||
스타일 & 해상도
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* 속성 탭 */}
|
|
||||||
<TabsContent value="properties" className="mt-0 flex-1 overflow-y-auto p-2">
|
|
||||||
<div className="space-y-2 text-xs">
|
|
||||||
{/* 기본 설정 */}
|
|
||||||
{renderBasicTab()}
|
|
||||||
|
|
||||||
{/* 상세 설정 통합 */}
|
|
||||||
<Separator className="my-2" />
|
|
||||||
{renderDetailTab()}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* 스타일 & 해상도 탭 */}
|
|
||||||
<TabsContent value="styles" className="mt-0 flex-1 overflow-y-auto">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* 해상도 설정 */}
|
|
||||||
{currentResolution && onResolutionChange && (
|
{currentResolution && onResolutionChange && (
|
||||||
<div className="border-b pb-2 px-2">
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Monitor className="h-3 w-3 text-primary" />
|
||||||
|
<h4 className="text-xs font-semibold">해상도 설정</h4>
|
||||||
|
</div>
|
||||||
<ResolutionPanel
|
<ResolutionPanel
|
||||||
currentResolution={currentResolution}
|
currentResolution={currentResolution}
|
||||||
onResolutionChange={onResolutionChange}
|
onResolutionChange={onResolutionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 기본 설정 */}
|
||||||
|
{renderBasicTab()}
|
||||||
|
|
||||||
|
{/* 상세 설정 */}
|
||||||
|
<Separator className="my-2" />
|
||||||
|
{renderDetailTab()}
|
||||||
|
|
||||||
{/* 스타일 설정 */}
|
{/* 스타일 설정 */}
|
||||||
{selectedComponent ? (
|
{selectedComponent && (
|
||||||
<div>
|
<>
|
||||||
<div className="mb-1.5 flex items-center gap-1.5 px-2">
|
<Separator className="my-2" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<Palette className="h-3 w-3 text-primary" />
|
<Palette className="h-3 w-3 text-primary" />
|
||||||
<h4 className="text-xs font-semibold">컴포넌트 스타일</h4>
|
<h4 className="text-xs font-semibold">컴포넌트 스타일</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -658,14 +659,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</>
|
||||||
<div className="flex h-full items-center justify-center text-muted-foreground text-xs">
|
|
||||||
컴포넌트를 선택하여 스타일을 편집하세요
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,8 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 옵션 - 가로 배치 */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{/* 라인 넘버 표시 */}
|
{/* 라인 넘버 표시 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="lineNumbers" className="text-sm font-medium">
|
<Label htmlFor="lineNumbers" className="text-sm font-medium">
|
||||||
|
|
@ -221,6 +223,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
|
||||||
onCheckedChange={(checked) => updateConfig("autoFormat", !!checked)}
|
onCheckedChange={(checked) => updateConfig("autoFormat", !!checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 플레이스홀더 */}
|
{/* 플레이스홀더 */}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,19 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
|
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
|
||||||
const { height: _height, ...styleWithoutHeight } = component.style || {};
|
const { height: _height, ...styleWithoutHeight } = component.style || {};
|
||||||
|
|
||||||
|
// 숨김 값 추출 (디버깅)
|
||||||
|
const hiddenValue = component.hidden || component.componentConfig?.hidden;
|
||||||
|
if (hiddenValue) {
|
||||||
|
console.log("🔍 DynamicComponentRenderer hidden 체크:", {
|
||||||
|
componentId: component.id,
|
||||||
|
componentType,
|
||||||
|
componentHidden: component.hidden,
|
||||||
|
componentConfigHidden: component.componentConfig?.hidden,
|
||||||
|
finalHiddenValue: hiddenValue,
|
||||||
|
isDesignMode: props.isDesignMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const rendererProps = {
|
const rendererProps = {
|
||||||
component,
|
component,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
|
@ -253,8 +266,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
componentConfig: component.componentConfig,
|
componentConfig: component.componentConfig,
|
||||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||||
// 새로운 기능들 전달
|
// 새로운 기능들 전달
|
||||||
autoGeneration: component.autoGeneration,
|
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
||||||
hidden: component.hidden,
|
hidden: hiddenValue,
|
||||||
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
|
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
|
||||||
isInteractive,
|
isInteractive,
|
||||||
formData,
|
formData,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
console.log("🔵 ButtonPrimaryComponent 렌더링, 받은 props:", {
|
||||||
|
componentId: component.id,
|
||||||
|
hasSelectedRowsData: !!selectedRowsData,
|
||||||
|
selectedRowsDataLength: selectedRowsData?.length,
|
||||||
|
selectedRowsData,
|
||||||
|
tableName,
|
||||||
|
screenId,
|
||||||
|
});
|
||||||
|
|
||||||
// 확인 다이얼로그 상태
|
// 확인 다이얼로그 상태
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [pendingAction, setPendingAction] = useState<{
|
const [pendingAction, setPendingAction] = useState<{
|
||||||
|
|
@ -204,7 +213,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// 확인 다이얼로그가 필요한 액션 타입들
|
// 확인 다이얼로그가 필요한 액션 타입들
|
||||||
const confirmationRequiredActions: ButtonActionType[] = ["save", "submit", "delete"];
|
const confirmationRequiredActions: ButtonActionType[] = ["save", "delete"];
|
||||||
|
|
||||||
// 실제 액션 실행 함수
|
// 실제 액션 실행 함수
|
||||||
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
|
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
|
||||||
|
|
@ -221,8 +230,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
// 추가 안전장치: 모든 로딩 토스트 제거
|
// 추가 안전장치: 모든 로딩 토스트 제거
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
|
|
||||||
// edit 액션을 제외하고만 로딩 토스트 표시
|
// UI 전환 액션(edit, modal, navigate)을 제외하고만 로딩 토스트 표시
|
||||||
if (actionConfig.type !== "edit") {
|
const silentActions = ["edit", "modal", "navigate"];
|
||||||
|
if (!silentActions.includes(actionConfig.type)) {
|
||||||
console.log("📱 로딩 토스트 표시 시작");
|
console.log("📱 로딩 토스트 표시 시작");
|
||||||
currentLoadingToastRef.current = toast.loading(
|
currentLoadingToastRef.current = toast.loading(
|
||||||
actionConfig.type === "save"
|
actionConfig.type === "save"
|
||||||
|
|
@ -237,9 +247,16 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current);
|
console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current);
|
||||||
|
} else {
|
||||||
|
console.log("🔕 UI 전환 액션은 로딩 토스트 표시 안함:", actionConfig.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
|
||||||
|
console.log("🔍 actionConfig 확인:", {
|
||||||
|
type: actionConfig.type,
|
||||||
|
successMessage: actionConfig.successMessage,
|
||||||
|
errorMessage: actionConfig.errorMessage,
|
||||||
|
});
|
||||||
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
||||||
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
|
||||||
|
|
||||||
|
|
@ -252,37 +269,70 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
|
|
||||||
// 실패한 경우 오류 처리
|
// 실패한 경우 오류 처리
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
// UI 전환 액션(edit, modal, navigate)은 에러도 조용히 처리
|
||||||
|
const silentActions = ["edit", "modal", "navigate"];
|
||||||
|
if (silentActions.includes(actionConfig.type)) {
|
||||||
|
console.log("🔕 UI 전환 액션 실패지만 에러 토스트 표시 안함:", actionConfig.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("❌ 액션 실패, 오류 토스트 표시");
|
console.log("❌ 액션 실패, 오류 토스트 표시");
|
||||||
const errorMessage =
|
// 기본 에러 메시지 결정
|
||||||
actionConfig.errorMessage ||
|
const defaultErrorMessage =
|
||||||
(actionConfig.type === "save"
|
actionConfig.type === "save"
|
||||||
? "저장 중 오류가 발생했습니다."
|
? "저장 중 오류가 발생했습니다."
|
||||||
: actionConfig.type === "delete"
|
: actionConfig.type === "delete"
|
||||||
? "삭제 중 오류가 발생했습니다."
|
? "삭제 중 오류가 발생했습니다."
|
||||||
: actionConfig.type === "submit"
|
: actionConfig.type === "submit"
|
||||||
? "제출 중 오류가 발생했습니다."
|
? "제출 중 오류가 발생했습니다."
|
||||||
: "처리 중 오류가 발생했습니다.");
|
: "처리 중 오류가 발생했습니다.";
|
||||||
|
|
||||||
|
// 커스텀 메시지 사용 조건:
|
||||||
|
// 1. 커스텀 메시지가 있고
|
||||||
|
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
|
||||||
|
const useCustomMessage =
|
||||||
|
actionConfig.errorMessage &&
|
||||||
|
(actionConfig.type === "save" || !actionConfig.errorMessage.includes("저장"));
|
||||||
|
|
||||||
|
const errorMessage = useCustomMessage ? actionConfig.errorMessage : defaultErrorMessage;
|
||||||
|
|
||||||
|
console.log("🔍 에러 메시지 결정:", {
|
||||||
|
actionType: actionConfig.type,
|
||||||
|
customMessage: actionConfig.errorMessage,
|
||||||
|
useCustom: useCustomMessage,
|
||||||
|
finalMessage: errorMessage
|
||||||
|
});
|
||||||
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 성공한 경우에만 성공 토스트 표시
|
// 성공한 경우에만 성공 토스트 표시
|
||||||
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
|
// edit, modal, navigate 액션은 조용히 처리 (UI 전환만 하므로 토스트 불필요)
|
||||||
if (actionConfig.type !== "edit") {
|
if (actionConfig.type !== "edit" && actionConfig.type !== "modal" && actionConfig.type !== "navigate") {
|
||||||
const successMessage =
|
// 기본 성공 메시지 결정
|
||||||
actionConfig.successMessage ||
|
const defaultSuccessMessage =
|
||||||
(actionConfig.type === "save"
|
actionConfig.type === "save"
|
||||||
? "저장되었습니다."
|
? "저장되었습니다."
|
||||||
: actionConfig.type === "delete"
|
: actionConfig.type === "delete"
|
||||||
? "삭제되었습니다."
|
? "삭제되었습니다."
|
||||||
: actionConfig.type === "submit"
|
: actionConfig.type === "submit"
|
||||||
? "제출되었습니다."
|
? "제출되었습니다."
|
||||||
: "완료되었습니다.");
|
: "완료되었습니다.";
|
||||||
|
|
||||||
|
// 커스텀 메시지 사용 조건:
|
||||||
|
// 1. 커스텀 메시지가 있고
|
||||||
|
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
|
||||||
|
const useCustomMessage =
|
||||||
|
actionConfig.successMessage &&
|
||||||
|
(actionConfig.type === "save" || !actionConfig.successMessage.includes("저장"));
|
||||||
|
|
||||||
|
const successMessage = useCustomMessage ? actionConfig.successMessage : defaultSuccessMessage;
|
||||||
|
|
||||||
console.log("🎉 성공 토스트 표시:", successMessage);
|
console.log("🎉 성공 토스트 표시:", successMessage);
|
||||||
toast.success(successMessage);
|
toast.success(successMessage);
|
||||||
} else {
|
} else {
|
||||||
console.log("🔕 edit 액션은 조용히 처리 (토스트 없음)");
|
console.log("🔕 UI 전환 액션은 조용히 처리 (토스트 없음):", actionConfig.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
console.log("✅ 버튼 액션 실행 성공:", actionConfig.type);
|
||||||
|
|
@ -357,6 +407,13 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
requiresConfirmation: confirmationRequiredActions.includes(processedConfig.action.type),
|
requiresConfirmation: confirmationRequiredActions.includes(processedConfig.action.type),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
|
||||||
|
if (processedConfig.action.type === "delete" && (!selectedRowsData || selectedRowsData.length === 0)) {
|
||||||
|
console.log("⚠️ 삭제할 데이터가 선택되지 않았습니다.");
|
||||||
|
toast.warning("삭제할 항목을 먼저 선택해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const context: ButtonActionContext = {
|
const context: ButtonActionContext = {
|
||||||
formData: formData || {},
|
formData: formData || {},
|
||||||
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
originalData: originalData || {}, // 부분 업데이트용 원본 데이터 추가
|
||||||
|
|
@ -370,6 +427,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("🔍 버튼 액션 실행 전 context 확인:", {
|
||||||
|
hasSelectedRowsData: !!selectedRowsData,
|
||||||
|
selectedRowsDataLength: selectedRowsData?.length,
|
||||||
|
selectedRowsData,
|
||||||
|
tableName,
|
||||||
|
screenId,
|
||||||
|
formData,
|
||||||
|
});
|
||||||
|
|
||||||
// 확인이 필요한 액션인지 확인
|
// 확인이 필요한 액션인지 확인
|
||||||
if (confirmationRequiredActions.includes(processedConfig.action.type)) {
|
if (confirmationRequiredActions.includes(processedConfig.action.type)) {
|
||||||
console.log("📋 확인 다이얼로그 표시 중...");
|
console.log("📋 확인 다이얼로그 표시 중...");
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ComponentRendererProps } from "../../types";
|
import { ComponentRendererProps } from "../../types";
|
||||||
import { TextDisplayConfig } from "./types";
|
import { TextDisplayConfig } from "./types";
|
||||||
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||||
|
|
||||||
export interface TextDisplayComponentProps extends ComponentRendererProps {
|
export interface TextDisplayComponentProps extends ComponentRendererProps {
|
||||||
// 추가 props가 필요한 경우 여기에 정의
|
// 추가 props가 필요한 경우 여기에 정의
|
||||||
|
|
@ -53,20 +54,7 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
// DOM props 필터링 (React 관련 props 제거)
|
// DOM props 필터링 (React 관련 props 제거)
|
||||||
const {
|
const domProps = filterDOMProps(props);
|
||||||
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 = {
|
const textStyle: React.CSSProperties = {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,19 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
// 숨김 상태 (props에서 전달받은 값 우선 사용)
|
||||||
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
|
||||||
|
|
||||||
|
// 디버깅: 컴포넌트 설정 확인
|
||||||
|
console.log("👻 텍스트 입력 컴포넌트 상태:", {
|
||||||
|
componentId: component.id,
|
||||||
|
label: component.label,
|
||||||
|
isHidden,
|
||||||
|
componentConfig: componentConfig,
|
||||||
|
readonly: componentConfig.readonly,
|
||||||
|
disabled: componentConfig.disabled,
|
||||||
|
required: componentConfig.required,
|
||||||
|
isDesignMode,
|
||||||
|
willRender: !(isHidden && !isDesignMode),
|
||||||
|
});
|
||||||
|
|
||||||
// 자동생성된 값 상태
|
// 자동생성된 값 상태
|
||||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||||
|
|
||||||
|
|
@ -134,18 +147,22 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}
|
}
|
||||||
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
|
||||||
|
|
||||||
|
// 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음
|
||||||
|
if (isHidden && !isDesignMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
|
||||||
const componentStyle: React.CSSProperties = {
|
const componentStyle: React.CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
...component.style,
|
...component.style,
|
||||||
...style,
|
...style,
|
||||||
// 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김
|
// 숨김 기능: 편집 모드에서만 연하게 표시
|
||||||
...(isHidden && {
|
...(isHidden && isDesignMode && {
|
||||||
opacity: isDesignMode ? 0.4 : 0,
|
opacity: 0.4,
|
||||||
backgroundColor: isDesignMode ? "#f3f4f6" : "transparent",
|
backgroundColor: "#f3f4f6",
|
||||||
pointerEvents: isDesignMode ? "auto" : "none",
|
pointerEvents: "auto",
|
||||||
display: isDesignMode ? "block" : "none",
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -315,7 +332,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// 이메일 타입 전용 UI
|
// 이메일 타입 전용 UI
|
||||||
if (webType === "email") {
|
if (webType === "email") {
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -417,7 +434,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// 전화번호 타입 전용 UI
|
// 전화번호 타입 전용 UI
|
||||||
if (webType === "tel") {
|
if (webType === "tel") {
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -498,7 +515,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// URL 타입 전용 UI
|
// URL 타입 전용 UI
|
||||||
if (webType === "url") {
|
if (webType === "url") {
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -553,7 +570,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// textarea 타입인 경우 별도 렌더링
|
// textarea 타입인 경우 별도 렌더링
|
||||||
if (webType === "textarea") {
|
if (webType === "textarea") {
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -594,7 +611,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
<label className="absolute -top-6 left-0 text-sm font-medium text-slate-600">
|
||||||
|
|
@ -644,7 +661,15 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
|
||||||
className={`box-border h-full w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-all duration-200 outline-none ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300"} ${componentConfig.disabled ? "bg-gray-100 text-gray-400" : "bg-white text-gray-900"} placeholder:text-gray-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:cursor-not-allowed`}
|
className={`box-border h-full w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-all duration-200 outline-none ${isSelected ? "border-blue-500 ring-2 ring-blue-100" : "border-gray-300"} ${componentConfig.disabled ? "bg-gray-100 text-gray-400" : "bg-white text-gray-900"} placeholder:text-gray-400 focus:border-orange-500 focus:ring-2 focus:ring-orange-100 disabled:cursor-not-allowed`}
|
||||||
onClick={handleClick}
|
onClick={(e) => {
|
||||||
|
console.log("🖱️ Input 클릭됨:", {
|
||||||
|
componentId: component.id,
|
||||||
|
disabled: componentConfig.disabled,
|
||||||
|
readOnly: componentConfig.readonly,
|
||||||
|
autoGenEnabled: testAutoGeneration.enabled,
|
||||||
|
});
|
||||||
|
handleClick(e);
|
||||||
|
}}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -47,41 +47,13 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<div className="mb-3 text-sm font-medium">고급 기능</div>
|
<div className="mb-3 text-sm font-medium">고급 기능</div>
|
||||||
|
|
||||||
{/* 숨김 기능 */}
|
{/* 숨김 기능 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="hidden">숨김 (편집기에서는 연하게, 실제 화면에서는 숨김)</Label>
|
<Label htmlFor="hidden">숨김</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="hidden"
|
id="hidden"
|
||||||
checked={config.hidden || false}
|
checked={config.hidden || false}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,11 @@ import type { ExtendedControlContext } from "@/types/control-management";
|
||||||
*/
|
*/
|
||||||
export type ButtonActionType =
|
export type ButtonActionType =
|
||||||
| "save" // 저장
|
| "save" // 저장
|
||||||
| "cancel" // 취소
|
|
||||||
| "delete" // 삭제
|
| "delete" // 삭제
|
||||||
| "edit" // 편집
|
| "edit" // 편집
|
||||||
| "add" // 추가
|
|
||||||
| "search" // 검색
|
|
||||||
| "reset" // 초기화
|
|
||||||
| "submit" // 제출
|
|
||||||
| "close" // 닫기
|
|
||||||
| "popup" // 팝업 열기
|
|
||||||
| "navigate" // 페이지 이동
|
| "navigate" // 페이지 이동
|
||||||
| "modal" // 모달 열기
|
| "modal" // 모달 열기
|
||||||
| "newWindow"; // 새 창 열기
|
| "control"; // 제어 흐름
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 버튼 액션 설정
|
* 버튼 액션 설정
|
||||||
|
|
@ -92,42 +85,18 @@ export class ButtonActionExecutor {
|
||||||
case "save":
|
case "save":
|
||||||
return await this.handleSave(config, context);
|
return await this.handleSave(config, context);
|
||||||
|
|
||||||
case "submit":
|
|
||||||
return await this.handleSubmit(config, context);
|
|
||||||
|
|
||||||
case "delete":
|
case "delete":
|
||||||
return await this.handleDelete(config, context);
|
return await this.handleDelete(config, context);
|
||||||
|
|
||||||
case "reset":
|
|
||||||
return this.handleReset(config, context);
|
|
||||||
|
|
||||||
case "cancel":
|
|
||||||
return this.handleCancel(config, context);
|
|
||||||
|
|
||||||
case "navigate":
|
case "navigate":
|
||||||
return this.handleNavigate(config, context);
|
return this.handleNavigate(config, context);
|
||||||
|
|
||||||
case "modal":
|
case "modal":
|
||||||
return this.handleModal(config, context);
|
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":
|
case "edit":
|
||||||
return this.handleEdit(config, context);
|
return this.handleEdit(config, context);
|
||||||
|
|
||||||
case "close":
|
|
||||||
return this.handleClose(config, context);
|
|
||||||
|
|
||||||
case "control":
|
case "control":
|
||||||
return this.handleControl(config, context);
|
return this.handleControl(config, context);
|
||||||
|
|
||||||
|
|
@ -515,9 +484,9 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.dispatchEvent(modalEvent);
|
window.dispatchEvent(modalEvent);
|
||||||
toast.success("모달 화면이 열렸습니다.");
|
// 모달 열기는 조용히 처리 (토스트 불필요)
|
||||||
} else {
|
} else {
|
||||||
toast.error("모달로 열 화면이 지정되지 않았습니다.");
|
console.error("모달로 열 화면이 지정되지 않았습니다.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1421,26 +1390,12 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
||||||
successMessage: "저장되었습니다.",
|
successMessage: "저장되었습니다.",
|
||||||
errorMessage: "저장 중 오류가 발생했습니다.",
|
errorMessage: "저장 중 오류가 발생했습니다.",
|
||||||
},
|
},
|
||||||
submit: {
|
|
||||||
type: "submit",
|
|
||||||
validateForm: true,
|
|
||||||
successMessage: "제출되었습니다.",
|
|
||||||
errorMessage: "제출 중 오류가 발생했습니다.",
|
|
||||||
},
|
|
||||||
delete: {
|
delete: {
|
||||||
type: "delete",
|
type: "delete",
|
||||||
confirmMessage: "정말 삭제하시겠습니까?",
|
confirmMessage: "정말 삭제하시겠습니까?",
|
||||||
successMessage: "삭제되었습니다.",
|
successMessage: "삭제되었습니다.",
|
||||||
errorMessage: "삭제 중 오류가 발생했습니다.",
|
errorMessage: "삭제 중 오류가 발생했습니다.",
|
||||||
},
|
},
|
||||||
reset: {
|
|
||||||
type: "reset",
|
|
||||||
confirmMessage: "초기화하시겠습니까?",
|
|
||||||
successMessage: "초기화되었습니다.",
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
type: "cancel",
|
|
||||||
},
|
|
||||||
navigate: {
|
navigate: {
|
||||||
type: "navigate",
|
type: "navigate",
|
||||||
},
|
},
|
||||||
|
|
@ -1448,29 +1403,11 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
||||||
type: "modal",
|
type: "modal",
|
||||||
modalSize: "md",
|
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: {
|
edit: {
|
||||||
type: "edit",
|
type: "edit",
|
||||||
successMessage: "편집되었습니다.",
|
successMessage: "편집되었습니다.",
|
||||||
},
|
},
|
||||||
close: {
|
control: {
|
||||||
type: "close",
|
type: "control",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue