컴포넌트 잘림현상 수정

This commit is contained in:
kjs 2025-10-23 15:06:00 +09:00
parent b104cd94f2
commit 70d2c96c80
10 changed files with 460 additions and 165 deletions

View File

@ -21,7 +21,7 @@ export default function ScreenViewPage() {
const [layout, setLayout] = useState<LayoutData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [formData, setFormData] = useState<Record<string, unknown>>({});
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
@ -191,6 +191,11 @@ export default function ScreenViewPage() {
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") &&
@ -227,6 +232,11 @@ export default function ScreenViewPage() {
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
formData={formData}
onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경 (자식):", fieldName, "=", value);
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
/>
);
})}

View File

@ -3,7 +3,7 @@
import React, { useState, useEffect } from "react";
import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -598,6 +598,48 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
}
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);
onSuccess();
onClose();

View File

@ -222,6 +222,66 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
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(() => {
@ -314,12 +374,20 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
}, [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 = {
position: "absolute" as const,
left: position?.x || 0,
top: position?.y || 0,
width: size?.width || 200,
height: size?.height || 40,
height: finalHeight,
zIndex: position?.z || 1,
...style,
};
@ -358,7 +426,10 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
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)}
@ -422,7 +493,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
console.log("🔍 RealtimePreview 최종 flowComponent:", flowComponent);
return (
<div className="h-full w-full">
<div className="h-auto w-full">
<FlowWidget component={flowComponent as any} />
</div>
);

View File

@ -42,6 +42,10 @@ interface RealtimePreviewProps {
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void;
refreshKey?: number;
onRefresh?: () => void;
// 폼 데이터 관련 props
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
@ -91,7 +95,88 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
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;
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
@ -120,6 +205,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
};
const getHeight = () => {
// 플로우 위젯의 경우 측정된 높이 사용
const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget";
if (isFlowWidget && actualHeight) {
return `${actualHeight}px`;
}
// 1순위: style.height가 있으면 우선 사용
if (componentStyle?.height) {
return componentStyle.height;
@ -175,7 +266,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
>
{/* 동적 컴포넌트 렌더링 */}
<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"
}`}
>
@ -198,6 +290,8 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
onSelectedRowsChange={onSelectedRowsChange}
refreshKey={refreshKey}
onRefresh={onRefresh}
formData={formData}
onFormDataChange={onFormDataChange}
/>
</div>

View File

@ -3895,6 +3895,73 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
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) {
return (
<div className="bg-background flex h-full items-center justify-center">
@ -4007,14 +4074,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
minHeight: Math.max(screenResolution.height, 800) * zoomLevel,
}}
>
{/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */}
{/* 실제 작업 캔버스 (해상도 크기) - 고정 크기 + 줌 적용 */}
<div
className="bg-background border-border border shadow-lg"
style={{
width: screenResolution.width,
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
minHeight: screenResolution.height,
transform: `scale(${zoomLevel})`, // 줌 레벨에 따라 시각적으로 확대/축소
width: `${screenResolution.width}px`,
height: `${Math.max(screenResolution.height, 800)}px`,
minWidth: `${screenResolution.width}px`,
maxWidth: `${screenResolution.width}px`,
minHeight: `${screenResolution.height}px`,
flexShrink: 0,
transform: `scale(${zoomLevel})`,
transformOrigin: "top center",
}}
>

View File

@ -9,7 +9,6 @@ import { Separator } from "@/components/ui/separator";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
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 {
ComponentData,
@ -96,14 +95,36 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
}
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
// 컴포넌트가 선택되지 않았을 때
// 컴포넌트가 선택되지 않았을 때도 해상도 설정은 표시
if (!selectedComponent) {
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" />
<p className="text-[10px] text-gray-500"> </p>
<p className="text-[10px] text-gray-500"> </p>
</div>
</div>
</div>
</div>
);
}
@ -340,26 +361,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
</Collapsible>
{/* 옵션 */}
<div className="space-y-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>
<div className="grid grid-cols-2 gap-2">
{widget.required !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
checked={widget.required === true}
onCheckedChange={(checked) => handleUpdate("required", checked)}
checked={widget.required === true || selectedComponent.componentConfig?.required === true}
onCheckedChange={(checked) => handleUpdate("componentConfig.required", checked)}
/>
<Label> </Label>
</div>
@ -367,8 +374,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
{widget.readonly !== undefined && (
<div className="flex items-center space-x-2">
<Checkbox
checked={widget.readonly === true}
onCheckedChange={(checked) => handleUpdate("readonly", checked)}
checked={widget.readonly === true || selectedComponent.componentConfig?.readonly === true}
onCheckedChange={(checked) => handleUpdate("componentConfig.readonly", checked)}
/>
<Label> </Label>
</div>
@ -605,47 +612,39 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
)}
</div>
{/* 탭 컨텐츠 */}
<Tabs defaultValue="properties" className="flex flex-1 flex-col overflow-hidden">
<TabsList className="grid h-7 w-full flex-shrink-0 grid-cols-2">
<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">
{/* 해상도 설정 */}
{/* 통합 컨텐츠 (탭 제거) */}
<div className="flex-1 overflow-y-auto p-2">
<div className="space-y-4 text-xs">
{/* 해상도 설정 - 항상 맨 위에 표시 */}
{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
currentResolution={currentResolution}
onResolutionChange={onResolutionChange}
/>
</div>
<Separator className="my-2" />
</>
)}
{/* 기본 설정 */}
{renderBasicTab()}
{/* 상세 설정 */}
<Separator className="my-2" />
{renderDetailTab()}
{/* 스타일 설정 */}
{selectedComponent ? (
<div>
<div className="mb-1.5 flex items-center gap-1.5 px-2">
{selectedComponent && (
<>
<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" />
<h4 className="text-xs font-semibold"> </h4>
</div>
@ -660,14 +659,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
}}
/>
</div>
) : (
<div className="flex h-full items-center justify-center text-muted-foreground text-xs">
</div>
</>
)}
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};

View File

@ -174,6 +174,8 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</div>
</div>
{/* 옵션 - 가로 배치 */}
<div className="grid grid-cols-2 gap-3">
{/* 라인 넘버 표시 */}
<div className="flex items-center justify-between">
<Label htmlFor="lineNumbers" className="text-sm font-medium">
@ -221,6 +223,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
onCheckedChange={(checked) => updateConfig("autoFormat", !!checked)}
/>
</div>
</div>
{/* 플레이스홀더 */}
<div>

View File

@ -240,6 +240,19 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
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 = {
component,
isSelected,
@ -253,8 +266,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
componentConfig: component.componentConfig,
value: currentValue, // formData에서 추출한 현재 값 전달
// 새로운 기능들 전달
autoGeneration: component.autoGeneration,
hidden: component.hidden,
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
hidden: hiddenValue,
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
isInteractive,
formData,

View File

@ -51,6 +51,19 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 숨김 상태 (props에서 전달받은 값 우선 사용)
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>("");
@ -134,18 +147,22 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
}
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
// 실제 화면에서 숨김 처리된 컴포넌트는 렌더링하지 않음
if (isHidden && !isDesignMode) {
return null;
}
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
const componentStyle: React.CSSProperties = {
width: "100%",
height: "100%",
...component.style,
...style,
// 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김
...(isHidden && {
opacity: isDesignMode ? 0.4 : 0,
backgroundColor: isDesignMode ? "#f3f4f6" : "transparent",
pointerEvents: isDesignMode ? "auto" : "none",
display: isDesignMode ? "block" : "none",
// 숨김 기능: 편집 모드에서만 연하게 표시
...(isHidden && isDesignMode && {
opacity: 0.4,
backgroundColor: "#f3f4f6",
pointerEvents: "auto",
}),
};
@ -315,7 +332,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
// 이메일 타입 전용 UI
if (webType === "email") {
return (
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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
if (webType === "tel") {
return (
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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
if (webType === "url") {
return (
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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 타입인 경우 별도 렌더링
if (webType === "textarea") {
return (
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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 (
<div className={`relative w-full ${className || ""}`} {...safeDomProps}>
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
{/* 라벨 렌더링 */}
{component.label && component.style?.labelDisplay !== false && (
<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}
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`}
onClick={handleClick}
onClick={(e) => {
console.log("🖱️ Input 클릭됨:", {
componentId: component.id,
disabled: componentConfig.disabled,
readOnly: componentConfig.readonly,
autoGenEnabled: testAutoGeneration.enabled,
});
handleClick(e);
}}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onChange={(e) => {

View File

@ -47,41 +47,13 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ conf
/>
</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="mb-3 text-sm font-medium"> </div>
{/* 숨김 기능 */}
<div className="space-y-2">
<Label htmlFor="hidden"> ( , )</Label>
<Label htmlFor="hidden"></Label>
<Checkbox
id="hidden"
checked={config.hidden || false}