버튼 수정

This commit is contained in:
kjs 2025-11-04 11:41:20 +09:00
parent 7aecae559b
commit d64ca5a8c0
7 changed files with 256 additions and 133 deletions

View File

@ -26,7 +26,7 @@ export default function ScreenViewPage() {
// 🆕 현재 로그인한 사용자 정보 // 🆕 현재 로그인한 사용자 정보
const { user, userName, companyCode } = useAuth(); const { user, userName, companyCode } = useAuth();
// 🆕 모바일 환경 감지 // 🆕 모바일 환경 감지
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
@ -189,10 +189,10 @@ export default function ScreenViewPage() {
if (loading) { if (loading) {
return ( return (
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50"> <div className="from-muted to-muted/50 flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br">
<div className="rounded-xl border border-border bg-background p-8 text-center shadow-lg"> <div className="border-border bg-background rounded-xl border p-8 text-center shadow-lg">
<Loader2 className="mx-auto h-10 w-10 animate-spin text-primary" /> <Loader2 className="text-primary mx-auto h-10 w-10 animate-spin" />
<p className="mt-4 font-medium text-foreground"> ...</p> <p className="text-foreground mt-4 font-medium"> ...</p>
</div> </div>
</div> </div>
); );
@ -200,13 +200,13 @@ export default function ScreenViewPage() {
if (error || !screen) { if (error || !screen) {
return ( return (
<div className="flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br from-muted to-muted/50"> <div className="from-muted to-muted/50 flex h-full min-h-[400px] w-full items-center justify-center bg-gradient-to-br">
<div className="max-w-md rounded-xl border border-border bg-background p-8 text-center shadow-lg"> <div className="border-border bg-background max-w-md rounded-xl border p-8 text-center shadow-lg">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-destructive/20 to-warning/20 shadow-sm"> <div className="from-destructive/20 to-warning/20 mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br shadow-sm">
<span className="text-3xl"></span> <span className="text-3xl"></span>
</div> </div>
<h2 className="mb-3 text-xl font-bold text-foreground"> </h2> <h2 className="text-foreground mb-3 text-xl font-bold"> </h2>
<p className="mb-6 leading-relaxed text-muted-foreground">{error || "요청하신 화면이 존재하지 않습니다."}</p> <p className="text-muted-foreground mb-6 leading-relaxed">{error || "요청하신 화면이 존재하지 않습니다."}</p>
<Button onClick={() => router.back()} variant="outline" className="rounded-lg"> <Button onClick={() => router.back()} variant="outline" className="rounded-lg">
</Button> </Button>
@ -225,7 +225,7 @@ export default function ScreenViewPage() {
{/* 절대 위치 기반 렌더링 */} {/* 절대 위치 기반 렌더링 */}
{layout && layout.components.length > 0 ? ( {layout && layout.components.length > 0 ? (
<div <div
className="bg-background relative origin-top-left h-full flex justify-start items-start" className="bg-background relative flex h-full origin-top-left items-start justify-start"
style={{ style={{
transform: `scale(${scale})`, transform: `scale(${scale})`,
transformOrigin: "top left", transformOrigin: "top left",
@ -238,27 +238,76 @@ export default function ScreenViewPage() {
// 🆕 플로우 버튼 그룹 감지 및 처리 // 🆕 플로우 버튼 그룹 감지 및 처리
const topLevelComponents = layout.components.filter((component) => !component.parentId); const topLevelComponents = layout.components.filter((component) => !component.parentId);
// 버튼은 scale에 맞춰 위치만 조정하면 됨 (scale = 1.0이면 그대로, scale < 1.0이면 왼쪽으로)
// 하지만 x=0 컴포넌트는 width: 100%로 확장되므로, 그만큼 버튼을 오른쪽으로 이동
const leftmostComponent = topLevelComponents.find((c) => c.position.x === 0);
let widthOffset = 0;
if (leftmostComponent && containerWidth > 0) {
const originalWidth = leftmostComponent.size?.width || screenWidth;
const actualWidth = containerWidth / scale;
widthOffset = Math.max(0, actualWidth - originalWidth);
console.log("📊 widthOffset 계산:", {
containerWidth,
scale,
screenWidth,
originalWidth,
actualWidth,
widthOffset,
leftmostType: leftmostComponent.type,
});
}
const buttonGroups: Record<string, any[]> = {}; const buttonGroups: Record<string, any[]> = {};
const processedButtonIds = new Set<string>(); const processedButtonIds = new Set<string>();
// 🔍 전체 버튼 목록 확인
const allButtons = topLevelComponents.filter((component) => {
const isButton =
(component.type === "component" &&
["button-primary", "button-secondary"].includes((component as any).componentType)) ||
(component.type === "widget" && (component as any).widgetType === "button");
return isButton;
});
console.log(
"🔍 메뉴에서 발견된 전체 버튼:",
allButtons.map((b) => ({
id: b.id,
label: b.label,
positionX: b.position.x,
positionY: b.position.y,
})),
);
topLevelComponents.forEach((component) => { topLevelComponents.forEach((component) => {
const isButton = const isButton =
component.type === "button" ||
(component.type === "component" && (component.type === "component" &&
["button-primary", "button-secondary"].includes((component as any).componentType)); ["button-primary", "button-secondary"].includes((component as any).componentType)) ||
(component.type === "widget" && (component as any).widgetType === "button");
if (isButton) { if (isButton) {
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
| FlowVisibilityConfig | FlowVisibilityConfig
| undefined; | undefined;
if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) { // 🔧 임시: 버튼 그룹 기능 완전 비활성화
// TODO: 사용자가 명시적으로 그룹을 원하는 경우에만 활성화하도록 UI 개선 필요
const DISABLE_BUTTON_GROUPS = true;
if (
!DISABLE_BUTTON_GROUPS &&
flowConfig?.enabled &&
flowConfig.layoutBehavior === "auto-compact" &&
flowConfig.groupId
) {
if (!buttonGroups[flowConfig.groupId]) { if (!buttonGroups[flowConfig.groupId]) {
buttonGroups[flowConfig.groupId] = []; buttonGroups[flowConfig.groupId] = [];
} }
buttonGroups[flowConfig.groupId].push(component); buttonGroups[flowConfig.groupId].push(component);
processedButtonIds.add(component.id); processedButtonIds.add(component.id);
} }
// else: 모든 버튼을 개별 렌더링
} }
}); });
@ -267,92 +316,121 @@ export default function ScreenViewPage() {
return ( return (
<> <>
{/* 일반 컴포넌트들 */} {/* 일반 컴포넌트들 */}
{regularComponents.map((component) => ( {regularComponents.map((component) => {
<RealtimePreview // 버튼인 경우 위치 조정 (테이블이 늘어난 만큼 오른쪽으로 이동)
key={component.id} const isButton =
component={component} (component.type === "component" &&
isSelected={false} ["button-primary", "button-secondary"].includes((component as any).componentType)) ||
isDesignMode={false} (component.type === "widget" && (component as any).widgetType === "button");
onClick={() => {}}
screenId={screenId}
tableName={screen?.tableName}
userId={user?.userId}
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
setFlowSelectedData(selectedData);
setFlowSelectedStepId(stepId);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
flowRefreshKey={flowRefreshKey}
onFlowRefresh={() => {
setFlowRefreshKey((prev) => prev + 1);
setFlowSelectedData([]); // 선택 해제
setFlowSelectedStepId(null);
}}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
>
{/* 자식 컴포넌트들 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
.map((child) => {
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
const relativeChildComponent = {
...child,
position: {
x: child.position.x - component.position.x,
y: child.position.y - component.position.y,
z: child.position.z || 1,
},
};
return ( const adjustedComponent =
<RealtimePreview isButton && widthOffset > 0
key={child.id} ? {
component={relativeChildComponent} ...component,
isSelected={false} position: {
isDesignMode={false} ...component.position,
onClick={() => {}} x: component.position.x + widthOffset,
screenId={screenId} },
tableName={screen?.tableName} }
userId={user?.userId} : component;
userName={userName}
companyCode={companyCode} // 버튼일 경우 로그 출력
selectedRowsData={selectedRowsData} if (isButton) {
onSelectedRowsChange={(_, selectedData) => { console.log("🔘 버튼 위치 조정:", {
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData); label: component.label,
setSelectedRowsData(selectedData); originalX: component.position.x,
}} adjustedX: component.position.x + widthOffset,
refreshKey={tableRefreshKey} widthOffset,
onRefresh={() => { });
console.log("🔄 테이블 새로고침 요청됨 (자식)"); }
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제 return (
}} <RealtimePreview
formData={formData} key={component.id}
onFormDataChange={(fieldName, value) => { component={adjustedComponent}
setFormData((prev) => ({ ...prev, [fieldName]: value })); isSelected={false}
}} isDesignMode={false}
/> onClick={() => {}}
); screenId={screenId}
})} tableName={screen?.tableName}
</RealtimePreview> userId={user?.userId}
))} userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
setFlowSelectedData(selectedData);
setFlowSelectedStepId(stepId);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
flowRefreshKey={flowRefreshKey}
onFlowRefresh={() => {
setFlowRefreshKey((prev) => prev + 1);
setFlowSelectedData([]); // 선택 해제
setFlowSelectedStepId(null);
}}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
>
{/* 자식 컴포넌트들 */}
{(component.type === "group" || component.type === "container" || component.type === "area") &&
layout.components
.filter((child) => child.parentId === component.id)
.map((child) => {
// 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정
const relativeChildComponent = {
...child,
position: {
x: child.position.x - component.position.x,
y: child.position.y - component.position.y,
z: child.position.z || 1,
},
};
return (
<RealtimePreview
key={child.id}
component={relativeChildComponent}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
screenId={screenId}
tableName={screen?.tableName}
userId={user?.userId}
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
onSelectedRowsChange={(_, selectedData) => {
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
setSelectedRowsData(selectedData);
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
console.log("🔄 테이블 새로고침 요청됨 (자식)");
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
/>
);
})}
</RealtimePreview>
);
})}
{/* 🆕 플로우 버튼 그룹들 */} {/* 🆕 플로우 버튼 그룹들 */}
{Object.entries(buttonGroups).map(([groupId, buttons]) => { {Object.entries(buttonGroups).map(([groupId, buttons]) => {
@ -372,6 +450,12 @@ export default function ScreenViewPage() {
{ x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 }, { x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 },
); );
// 버튼 그룹 위치에도 widthOffset 적용 (테이블이 늘어난 만큼 오른쪽으로 이동)
const adjustedGroupPosition = {
...groupPosition,
x: groupPosition.x + widthOffset,
};
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산 // 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
const direction = groupConfig.groupDirection || "horizontal"; const direction = groupConfig.groupDirection || "horizontal";
const gap = groupConfig.groupGap ?? 8; const gap = groupConfig.groupGap ?? 8;

View File

@ -1633,24 +1633,19 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
} }
}; };
return ( return applyStyles(
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
disabled={readonly} disabled={readonly}
size="sm" size="sm"
variant={config?.variant || "default"} variant={config?.variant || "default"}
className="w-full" className="w-full"
style={{ height: "100%" }}
style={{ style={{
// 컴포넌트 스타일과 설정 스타일 모두 적용
...comp.style,
// 크기는 className으로 처리하므로 CSS 크기 속성 제거
width: "100%",
height: "100%", height: "100%",
// 설정값이 있으면 우선 적용, 없으면 컴포넌트 스타일 사용 // 설정값이 있으면 우선 적용
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor, backgroundColor: config?.backgroundColor,
color: config?.textColor || comp.style?.color, color: config?.textColor,
borderColor: config?.borderColor || comp.style?.borderColor, borderColor: config?.borderColor,
}} }}
> >
{label || "버튼"} {label || "버튼"}

View File

@ -425,9 +425,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
disabled={config?.disabled} disabled={config?.disabled}
className="h-full w-full" className="h-full w-full"
style={{ style={{
backgroundColor: config?.backgroundColor, // 컴포넌트 스타일 먼저 적용
color: config?.textColor,
...comp.style, ...comp.style,
// 설정값이 있으면 우선 적용
backgroundColor: config?.backgroundColor || comp.style?.backgroundColor,
color: config?.textColor || comp.style?.color,
}} }}
> >
{label || "버튼"} {label || "버튼"}

View File

@ -235,17 +235,32 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
return `${size?.height || 40}px`; return `${size?.height || 40}px`;
}; };
// 버튼 컴포넌트인지 확인
const isButtonComponent =
(component.type === "widget" && (component as WidgetComponent).widgetType === "button") ||
(component.type === "component" && (component as any).componentType?.includes("button"));
// 버튼일 경우 로그 출력 (편집기)
if (isButtonComponent && isDesignMode) {
console.log("🎨 [편집기] 버튼 위치:", {
label: component.label,
positionX: position.x,
positionY: position.y,
sizeWidth: size?.width,
sizeHeight: size?.height,
});
}
const baseStyle = { const baseStyle = {
left: `${position.x}px`, left: `${position.x}px`,
top: `${position.y}px`, top: `${position.y}px`,
// 🆕 left가 0이면 부모 너비를 100% 채우도록 수정 (우측 여백 제거) // x=0인 컴포넌트는 전체 너비 사용 (버튼 제외)
width: position.x === 0 ? "100%" : getWidth(), width: (position.x === 0 && !isButtonComponent) ? "100%" : getWidth(),
height: getHeight(), // 모든 컴포넌트 고정 높이로 변경 height: getHeight(),
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 zIndex: component.type === "layout" ? 1 : position.z || 2,
...componentStyle, ...componentStyle,
// style.width가 있어도 position.x === 0이면 100%로 강제 // x=0인 컴포넌트는 100% 너비 강제 (버튼 제외)
...(position.x === 0 && { width: "100%" }), ...(position.x === 0 && !isButtonComponent && { width: "100%" }),
// right 속성 강제 제거
right: undefined, right: undefined,
}; };

View File

@ -1250,14 +1250,33 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
{/* 실제 컴포넌트 */} {/* 실제 컴포넌트 */}
<div <div
style={{ style={(() => {
position: "absolute", const style = {
left: `${component.position.x}px`, position: "absolute" as const,
top: `${component.position.y}px`, left: `${component.position.x}px`,
width: component.style?.width || `${component.size.width}px`, top: `${component.position.y}px`,
height: component.style?.height || `${component.size.height}px`, width: component.style?.width || `${component.size.width}px`,
zIndex: component.position.z || 1, height: component.style?.height || `${component.size.height}px`,
}} zIndex: component.position.z || 1,
};
// 버튼 타입일 때 디버깅 (widget 타입 또는 component 타입 모두 체크)
if (
(component.type === "widget" && (component as any).widgetType === "button") ||
(component.type === "component" && (component as any).componentType?.includes("button"))
) {
console.log("🔘 ScreenList 버튼 외부 div 스타일:", {
id: component.id,
label: component.label,
position: component.position,
size: component.size,
componentStyle: component.style,
appliedStyle: style,
});
}
return style;
})()}
> >
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */} {/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}
{component.type !== "widget" ? ( {component.type !== "widget" ? (

View File

@ -31,7 +31,11 @@ export const ButtonWidget: React.FC<WebTypeComponentProps> = ({
onClick={handleClick} onClick={handleClick}
disabled={disabled || readonly} disabled={disabled || readonly}
className={`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500 ${className || ""} `} className={`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500 ${className || ""} `}
style={style} style={{
...style,
width: "100%",
height: "100%",
}}
title={config?.tooltip || placeholder} title={config?.tooltip || placeholder}
> >
{config?.label || config?.text || value || placeholder || "버튼"} {config?.label || config?.text || value || placeholder || "버튼"}

View File

@ -21,12 +21,16 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
const { webTypes } = useWebTypes({ active: "Y" }); const { webTypes } = useWebTypes({ active: "Y" });
// 디버깅: 전달받은 웹타입과 props 정보 로깅 // 디버깅: 전달받은 웹타입과 props 정보 로깅
console.log("🔍 DynamicWebTypeRenderer 호출:", { if (webType === "button") {
webType, console.log("🔘 DynamicWebTypeRenderer 버튼 호출:", {
propsKeys: Object.keys(props), webType,
component: props.component, component: props.component,
isFileComponent: props.component?.type === "file" || webType === "file", position: props.component?.position,
}); size: props.component?.size,
style: props.component?.style,
config,
});
}
const webTypeDefinition = useMemo(() => { const webTypeDefinition = useMemo(() => {
return WebTypeRegistry.getWebType(webType); return WebTypeRegistry.getWebType(webType);