ERP-node/frontend/app/(main)/screens/[screenId]/page.tsx

732 lines
33 KiB
TypeScript
Raw Normal View History

2025-09-01 18:42:59 +09:00
"use client";
2025-10-16 18:16:57 +09:00
import React, { useEffect, useState } from "react";
2025-09-01 18:42:59 +09:00
import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
2025-09-01 18:42:59 +09:00
import { screenApi } from "@/lib/api/screen";
import { ScreenDefinition, LayoutData } from "@/types/screen";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
2025-09-12 14:24:25 +09:00
import { initializeComponents } from "@/lib/registry/components";
2025-09-18 18:49:30 +09:00
import { EditModal } from "@/components/screen/EditModal";
2025-10-22 17:19:47 +09:00
import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic";
2025-10-24 10:37:02 +09:00
import { FlowButtonGroup } from "@/components/screen/widgets/FlowButtonGroup";
import { FlowVisibilityConfig } from "@/types/control-management";
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
2025-10-28 15:39:22 +09:00
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
2025-10-29 11:26:00 +09:00
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지
2025-09-01 18:42:59 +09:00
export default function ScreenViewPage() {
const params = useParams();
const router = useRouter();
const screenId = parseInt(params.screenId as string);
2025-10-29 11:26:00 +09:00
// 🆕 현재 로그인한 사용자 정보
const { user, userName, companyCode } = useAuth();
2025-11-04 11:41:20 +09:00
// 🆕 모바일 환경 감지
const { isMobile } = useResponsive();
2025-10-29 11:26:00 +09:00
2025-09-01 18:42:59 +09:00
const [screen, setScreen] = useState<ScreenDefinition | null>(null);
const [layout, setLayout] = useState<LayoutData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
2025-10-23 15:06:00 +09:00
const [formData, setFormData] = useState<Record<string, unknown>>({});
2025-09-01 18:42:59 +09:00
2025-10-23 13:15:52 +09:00
// 테이블에서 선택된 행 데이터 (버튼 액션에 전달)
const [selectedRowsData, setSelectedRowsData] = useState<any[]>([]);
// 테이블 정렬 정보 (엑셀 다운로드용)
const [tableSortBy, setTableSortBy] = useState<string | undefined>();
const [tableSortOrder, setTableSortOrder] = useState<"asc" | "desc">("asc");
const [tableColumnOrder, setTableColumnOrder] = useState<string[] | undefined>();
2025-11-05 10:23:00 +09:00
const [tableDisplayData, setTableDisplayData] = useState<any[]>([]); // 화면에 표시된 데이터 (컬럼 순서 포함)
// 플로우에서 선택된 데이터 (버튼 액션에 전달)
const [flowSelectedData, setFlowSelectedData] = useState<any[]>([]);
const [flowSelectedStepId, setFlowSelectedStepId] = useState<number | null>(null);
2025-10-23 13:15:52 +09:00
// 테이블 새로고침을 위한 키 (값이 변경되면 테이블이 리렌더링됨)
const [tableRefreshKey, setTableRefreshKey] = useState(0);
2025-10-23 17:55:04 +09:00
// 플로우 새로고침을 위한 키 (값이 변경되면 플로우 데이터가 리렌더링됨)
const [flowRefreshKey, setFlowRefreshKey] = useState(0);
2025-09-18 18:49:30 +09:00
// 편집 모달 상태
const [editModalOpen, setEditModalOpen] = useState(false);
const [editModalConfig, setEditModalConfig] = useState<{
screenId?: number;
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
editData?: Record<string, unknown>;
2025-09-18 18:49:30 +09:00
onSave?: () => void;
modalTitle?: string;
modalDescription?: string;
2025-09-18 18:49:30 +09:00
}>({});
2025-11-04 11:47:46 +09:00
// 레이아웃 준비 완료 상태 (버튼 위치 계산 완료 후 화면 표시)
const [layoutReady, setLayoutReady] = useState(true);
2025-10-23 16:50:41 +09:00
const containerRef = React.useRef<HTMLDivElement>(null);
2025-10-24 16:34:21 +09:00
const [scale, setScale] = useState(1);
const [containerWidth, setContainerWidth] = useState(0);
2025-10-23 16:50:41 +09:00
2025-09-12 14:24:25 +09:00
useEffect(() => {
const initComponents = async () => {
try {
await initializeComponents();
} catch (error) {
console.error("❌ 할당된 화면에서 컴포넌트 시스템 초기화 실패:", error);
}
};
initComponents();
}, []);
2025-09-18 18:49:30 +09:00
// 편집 모달 이벤트 리스너 등록
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
console.log("🎭 편집 모달 열기 이벤트 수신:", event.detail);
setEditModalConfig({
screenId: event.detail.screenId,
modalSize: event.detail.modalSize,
editData: event.detail.editData,
onSave: event.detail.onSave,
modalTitle: event.detail.modalTitle,
modalDescription: event.detail.modalDescription,
2025-09-18 18:49:30 +09:00
});
setEditModalOpen(true);
};
// @ts-expect-error - CustomEvent type
2025-09-18 18:49:30 +09:00
window.addEventListener("openEditModal", handleOpenEditModal);
return () => {
// @ts-expect-error - CustomEvent type
2025-09-18 18:49:30 +09:00
window.removeEventListener("openEditModal", handleOpenEditModal);
};
}, []);
2025-09-01 18:42:59 +09:00
useEffect(() => {
const loadScreen = async () => {
try {
setLoading(true);
2025-11-04 11:47:46 +09:00
setLayoutReady(false); // 화면 로드 시 레이아웃 준비 초기화
2025-09-01 18:42:59 +09:00
setError(null);
// 화면 정보 로드
const screenData = await screenApi.getScreen(screenId);
setScreen(screenData);
// 레이아웃 로드
try {
const layoutData = await screenApi.getLayout(screenId);
setLayout(layoutData);
} catch (layoutError) {
console.warn("레이아웃 로드 실패, 빈 레이아웃 사용:", layoutError);
setLayout({
screenId,
2025-09-01 18:42:59 +09:00
components: [],
gridSettings: {
columns: 12,
gap: 16,
padding: 16,
enabled: true,
size: 8,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
2025-09-01 18:42:59 +09:00
});
}
} catch (error) {
console.error("화면 로드 실패:", error);
setError("화면을 불러오는데 실패했습니다.");
toast.error("화면을 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
};
if (screenId) {
loadScreen();
}
}, [screenId]);
2025-11-04 14:33:39 +09:00
// 🆕 autoFill 자동 입력 초기화
useEffect(() => {
const initAutoFill = async () => {
if (!layout || !layout.components || !user) {
return;
}
for (const comp of layout.components) {
// type: "component" 또는 type: "widget" 모두 처리
if (comp.type === 'widget' || comp.type === 'component') {
const widget = comp as any;
const fieldName = widget.columnName || widget.id;
// autoFill 처리
if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) {
const autoFillConfig = widget.autoFill || (comp as any).autoFill;
const currentValue = formData[fieldName];
if (currentValue === undefined || currentValue === '') {
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
// 사용자 정보에서 필터 값 가져오기
const userValue = user?.[userField as keyof typeof user];
if (userValue && sourceTable && filterColumn && displayColumn) {
try {
const { tableTypeApi } = await import("@/lib/api/screen");
const result = await tableTypeApi.getTableRecord(
sourceTable,
filterColumn,
userValue,
displayColumn
);
setFormData((prev) => ({
...prev,
[fieldName]: result.value,
}));
} catch (error) {
console.error(`autoFill 조회 실패: ${fieldName}`, error);
}
}
}
}
}
}
};
initAutoFill();
}, [layout, user]);
// 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) - 모바일에서는 비활성화
2025-10-23 16:50:41 +09:00
useEffect(() => {
// 모바일 환경에서는 스케일 조정 비활성화 (반응형만 작동)
if (isMobile) {
setScale(1);
return;
}
2025-10-23 16:50:41 +09:00
const updateScale = () => {
if (containerRef.current && layout) {
2025-10-24 16:34:21 +09:00
const designWidth = layout?.screenResolution?.width || 1200;
const designHeight = layout?.screenResolution?.height || 800;
// containerRef는 이미 패딩이 적용된 영역 내부이므로 offsetWidth는 패딩을 제외한 크기입니다
2025-10-23 16:50:41 +09:00
const containerWidth = containerRef.current.offsetWidth;
2025-10-24 16:34:21 +09:00
const containerHeight = containerRef.current.offsetHeight;
2025-10-23 16:50:41 +09:00
// 가로/세로 비율 중 작은 것을 선택하여 화면에 맞게 스케일 조정
// 하지만 화면이 컨테이너 전체 너비를 차지하도록 하기 위해 가로를 우선시
2025-10-24 16:34:21 +09:00
const scaleX = containerWidth / designWidth;
const scaleY = containerHeight / designHeight;
// 가로를 우선으로 하되, 세로가 넘치지 않도록 제한
2025-10-24 16:34:21 +09:00
const newScale = Math.min(scaleX, scaleY);
2025-10-23 16:50:41 +09:00
setScale(newScale);
// 컨테이너 너비 업데이트
setContainerWidth(containerWidth);
2025-11-04 11:47:46 +09:00
// 스케일 계산 완료 후 레이아웃 준비 완료 표시
setLayoutReady(true);
2025-10-23 16:50:41 +09:00
}
};
2025-10-24 16:34:21 +09:00
// 초기 측정
const timer = setTimeout(updateScale, 100);
2025-10-23 16:50:41 +09:00
window.addEventListener("resize", updateScale);
return () => {
clearTimeout(timer);
window.removeEventListener("resize", updateScale);
};
}, [layout, isMobile]);
2025-10-23 16:50:41 +09:00
2025-09-01 18:42:59 +09:00
if (loading) {
return (
2025-11-04 11:41:20 +09:00
<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="border-border bg-background rounded-xl border p-8 text-center shadow-lg">
<Loader2 className="text-primary mx-auto h-10 w-10 animate-spin" />
<p className="text-foreground mt-4 font-medium"> ...</p>
2025-09-01 18:42:59 +09:00
</div>
</div>
);
}
if (error || !screen) {
return (
2025-11-04 11:41:20 +09:00
<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="border-border bg-background max-w-md rounded-xl border p-8 text-center shadow-lg">
<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">
2025-09-29 17:21:47 +09:00
<span className="text-3xl"></span>
2025-09-01 18:42:59 +09:00
</div>
2025-11-04 11:41:20 +09:00
<h2 className="text-foreground mb-3 text-xl font-bold"> </h2>
<p className="text-muted-foreground mb-6 leading-relaxed">{error || "요청하신 화면이 존재하지 않습니다."}</p>
2025-09-29 17:21:47 +09:00
<Button onClick={() => router.back()} variant="outline" className="rounded-lg">
2025-09-01 18:42:59 +09:00
</Button>
</div>
</div>
);
}
2025-09-04 17:01:07 +09:00
// 화면 해상도 정보가 있으면 해당 크기로, 없으면 기본 크기 사용
const screenWidth = layout?.screenResolution?.width || 1200;
2025-10-22 17:19:47 +09:00
const screenHeight = layout?.screenResolution?.height || 800;
2025-09-04 17:01:07 +09:00
2025-09-01 18:42:59 +09:00
return (
2025-10-28 15:39:22 +09:00
<ScreenPreviewProvider isPreviewMode={false}>
<div ref={containerRef} className="bg-background h-full w-full overflow-hidden">
2025-11-04 11:47:46 +09:00
{/* 레이아웃 준비 중 로딩 표시 */}
{!layoutReady && (
<div className="from-muted to-muted/50 flex h-full w-full items-center justify-center bg-gradient-to-br">
<div className="border-border bg-background rounded-xl border p-8 text-center shadow-lg">
<Loader2 className="text-primary mx-auto h-8 w-8 animate-spin" />
<p className="text-foreground mt-4 text-sm font-medium"> ...</p>
</div>
</div>
)}
2025-10-28 15:39:22 +09:00
{/* 절대 위치 기반 렌더링 */}
2025-11-04 11:47:46 +09:00
{layoutReady && layout && layout.components.length > 0 ? (
2025-10-28 15:39:22 +09:00
<div
2025-11-04 11:41:20 +09:00
className="bg-background relative flex h-full origin-top-left items-start justify-start"
2025-10-28 15:39:22 +09:00
style={{
transform: `scale(${scale})`,
transformOrigin: "top left",
width: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
minWidth: containerWidth > 0 ? `${containerWidth / scale}px` : "100%",
2025-10-28 15:39:22 +09:00
}}
>
{/* 최상위 컴포넌트들 렌더링 */}
{(() => {
// 🆕 플로우 버튼 그룹 감지 및 처리
const topLevelComponents = layout.components.filter((component) => !component.parentId);
2025-11-04 11:41:20 +09:00
// 버튼은 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,
});
}
2025-10-28 15:39:22 +09:00
const buttonGroups: Record<string, any[]> = {};
const processedButtonIds = new Set<string>();
2025-11-04 11:41:20 +09:00
// 🔍 전체 버튼 목록 확인
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,
2025-11-05 16:36:32 +09:00
width: b.size?.width,
height: b.size?.height,
2025-11-04 11:41:20 +09:00
})),
);
2025-10-28 15:39:22 +09:00
topLevelComponents.forEach((component) => {
const isButton =
(component.type === "component" &&
2025-11-04 11:41:20 +09:00
["button-primary", "button-secondary"].includes((component as any).componentType)) ||
(component.type === "widget" && (component as any).widgetType === "button");
2025-10-28 15:39:22 +09:00
if (isButton) {
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
| FlowVisibilityConfig
| undefined;
2025-11-04 11:41:20 +09:00
// 🔧 임시: 버튼 그룹 기능 완전 비활성화
// TODO: 사용자가 명시적으로 그룹을 원하는 경우에만 활성화하도록 UI 개선 필요
2025-11-04 12:06:00 +09:00
const DISABLE_BUTTON_GROUPS = false;
2025-11-04 11:41:20 +09:00
if (
!DISABLE_BUTTON_GROUPS &&
flowConfig?.enabled &&
flowConfig.layoutBehavior === "auto-compact" &&
flowConfig.groupId
) {
2025-10-28 15:39:22 +09:00
if (!buttonGroups[flowConfig.groupId]) {
buttonGroups[flowConfig.groupId] = [];
}
buttonGroups[flowConfig.groupId].push(component);
processedButtonIds.add(component.id);
2025-10-24 10:37:02 +09:00
}
2025-11-04 11:41:20 +09:00
// else: 모든 버튼을 개별 렌더링
2025-10-24 10:37:02 +09:00
}
2025-10-28 15:39:22 +09:00
});
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
return (
<>
{/* 일반 컴포넌트들 */}
2025-11-04 11:41:20 +09:00
{regularComponents.map((component) => {
// 버튼인 경우 위치 조정 (테이블이 늘어난 만큼 오른쪽으로 이동)
const isButton =
(component.type === "component" &&
["button-primary", "button-secondary"].includes((component as any).componentType)) ||
(component.type === "widget" && (component as any).widgetType === "button");
const adjustedComponent =
isButton && widthOffset > 0
? {
...component,
position: {
...component.position,
x: component.position.x + widthOffset,
},
}
: component;
// 버튼일 경우 로그 출력
if (isButton) {
console.log("🔘 버튼 위치 조정:", {
label: component.label,
originalX: component.position.x,
adjustedX: component.position.x + widthOffset,
widthOffset,
});
}
return (
<RealtimePreview
key={component.id}
component={adjustedComponent}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
screenId={screenId}
tableName={screen?.tableName}
userId={user?.userId}
userName={userName}
companyCode={companyCode}
selectedRowsData={selectedRowsData}
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
2025-11-05 10:23:00 +09:00
tableDisplayData={tableDisplayData}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => {
2025-11-04 11:41:20 +09:00
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
console.log("📊 정렬 정보:", { sortBy, sortOrder, columnOrder });
2025-11-05 10:23:00 +09:00
console.log("📊 화면 표시 데이터:", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] });
2025-11-04 11:41:20 +09:00
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
2025-11-05 10:23:00 +09:00
setTableDisplayData(tableDisplayData || []);
2025-11-04 11:41:20 +09:00
}}
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}
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
2025-11-05 10:23:00 +09:00
tableDisplayData={tableDisplayData}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder, tableDisplayData) => {
2025-11-04 11:41:20 +09:00
console.log("🔍 화면에서 선택된 행 데이터 (자식):", selectedData);
console.log("📊 정렬 정보 (자식):", { sortBy, sortOrder, columnOrder });
2025-11-05 10:23:00 +09:00
console.log("📊 화면 표시 데이터 (자식):", { count: tableDisplayData?.length, firstRow: tableDisplayData?.[0] });
2025-11-04 11:41:20 +09:00
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
2025-11-05 10:23:00 +09:00
setTableDisplayData(tableDisplayData || []);
2025-11-04 11:41:20 +09:00
}}
refreshKey={tableRefreshKey}
onRefresh={() => {
console.log("🔄 테이블 새로고침 요청됨 (자식)");
setTableRefreshKey((prev) => prev + 1);
setSelectedRowsData([]); // 선택 해제
}}
formData={formData}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
/>
);
})}
</RealtimePreview>
);
})}
2025-10-28 15:39:22 +09:00
{/* 🆕 플로우 버튼 그룹들 */}
{Object.entries(buttonGroups).map(([groupId, buttons]) => {
if (buttons.length === 0) return null;
const firstButton = buttons[0];
const groupConfig = (firstButton as any).webTypeConfig
?.flowVisibilityConfig as FlowVisibilityConfig;
2025-11-04 12:06:00 +09:00
// 🔍 버튼 그룹 설정 확인
console.log("🔍 버튼 그룹 설정:", {
groupId,
buttonCount: buttons.length,
buttons: buttons.map((b) => ({
id: b.id,
label: b.label,
x: b.position.x,
y: b.position.y,
})),
groupConfig: {
layoutBehavior: groupConfig.layoutBehavior,
groupDirection: groupConfig.groupDirection,
groupAlign: groupConfig.groupAlign,
groupGap: groupConfig.groupGap,
},
});
// 🔧 수정: 그룹 컨테이너는 첫 번째 버튼 위치를 기준으로 하되,
// 각 버튼의 상대 위치는 원래 위치를 유지
const firstButtonPosition = {
x: buttons[0].position.x,
y: buttons[0].position.y,
z: buttons[0].position.z || 2,
};
2025-10-28 15:39:22 +09:00
2025-11-04 12:06:00 +09:00
// 버튼 그룹 위치에도 widthOffset 적용
2025-11-04 11:41:20 +09:00
const adjustedGroupPosition = {
2025-11-04 12:06:00 +09:00
...firstButtonPosition,
x: firstButtonPosition.x + widthOffset,
2025-11-04 11:41:20 +09:00
};
2025-10-28 15:39:22 +09:00
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
const direction = groupConfig.groupDirection || "horizontal";
const gap = groupConfig.groupGap ?? 8;
let groupWidth = 0;
let groupHeight = 0;
if (direction === "horizontal") {
groupWidth = buttons.reduce((total, button, index) => {
const buttonWidth = button.size?.width || 100;
const gapWidth = index < buttons.length - 1 ? gap : 0;
return total + buttonWidth + gapWidth;
}, 0);
groupHeight = Math.max(...buttons.map((b) => b.size?.height || 40));
} else {
groupWidth = Math.max(...buttons.map((b) => b.size?.width || 100));
groupHeight = buttons.reduce((total, button, index) => {
const buttonHeight = button.size?.height || 40;
const gapHeight = index < buttons.length - 1 ? gap : 0;
return total + buttonHeight + gapHeight;
}, 0);
}
return (
<div
key={`flow-button-group-${groupId}`}
style={{
position: "absolute",
2025-11-04 12:06:00 +09:00
left: `${adjustedGroupPosition.x}px`,
top: `${adjustedGroupPosition.y}px`,
zIndex: adjustedGroupPosition.z,
2025-10-28 15:39:22 +09:00
width: `${groupWidth}px`,
height: `${groupHeight}px`,
2025-10-24 10:37:02 +09:00
}}
2025-10-28 15:39:22 +09:00
>
<FlowButtonGroup
buttons={buttons}
groupConfig={groupConfig}
isDesignMode={false}
renderButton={(button) => {
2025-11-04 12:06:00 +09:00
// 🔧 각 버튼의 상대 위치 = 버튼의 원래 위치 - 첫 번째 버튼 위치
2025-10-28 15:39:22 +09:00
const relativeButton = {
...button,
2025-11-04 12:06:00 +09:00
position: {
x: button.position.x - firstButtonPosition.x,
y: button.position.y - firstButtonPosition.y,
z: button.position.z || 1,
},
2025-10-28 15:39:22 +09:00
};
return (
<div
key={button.id}
style={{
position: "relative",
display: "inline-block",
width: button.size?.width || 100,
2025-11-05 16:36:32 +09:00
minWidth: button.size?.width || 100,
2025-10-28 15:39:22 +09:00
height: button.size?.height || 40,
2025-11-05 16:36:32 +09:00
flexShrink: 0,
2025-10-28 15:39:22 +09:00
}}
>
<div style={{ width: "100%", height: "100%" }}>
<DynamicComponentRenderer
component={relativeButton}
isDesignMode={false}
isInteractive={true}
formData={formData}
onDataflowComplete={() => {}}
screenId={screenId}
tableName={screen?.tableName}
2025-10-29 11:26:00 +09:00
userId={user?.userId}
userName={userName}
companyCode={companyCode}
2025-11-05 10:23:00 +09:00
tableDisplayData={tableDisplayData}
2025-10-28 15:39:22 +09:00
selectedRowsData={selectedRowsData}
sortBy={tableSortBy}
sortOrder={tableSortOrder}
columnOrder={tableColumnOrder}
onSelectedRowsChange={(_, selectedData, sortBy, sortOrder, columnOrder) => {
2025-10-28 15:39:22 +09:00
setSelectedRowsData(selectedData);
setTableSortBy(sortBy);
setTableSortOrder(sortOrder || "asc");
setTableColumnOrder(columnOrder);
2025-10-28 15:39:22 +09:00
}}
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);
}}
onFormDataChange={(fieldName, value) => {
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
/>
</div>
</div>
);
}}
/>
</div>
);
})}
</>
);
})()}
</div>
) : (
// 빈 화면일 때
<div className="bg-background flex items-center justify-center" style={{ minHeight: screenHeight }}>
<div className="text-center">
<div className="bg-muted mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full shadow-sm">
<span className="text-2xl">📄</span>
</div>
<h2 className="text-foreground mb-2 text-xl font-semibold"> </h2>
<p className="text-muted-foreground"> .</p>
2025-09-01 18:42:59 +09:00
</div>
</div>
2025-10-28 15:39:22 +09:00
)}
{/* 편집 모달 */}
<EditModal
isOpen={editModalOpen}
onClose={() => {
setEditModalOpen(false);
setEditModalConfig({});
}}
screenId={editModalConfig.screenId}
modalSize={editModalConfig.modalSize}
editData={editModalConfig.editData}
onSave={editModalConfig.onSave}
modalTitle={editModalConfig.modalTitle}
modalDescription={editModalConfig.modalDescription}
onDataChange={(changedFormData) => {
console.log("📝 EditModal에서 데이터 변경 수신:", changedFormData);
// 변경된 데이터를 메인 폼에 반영
setFormData((prev) => {
const updatedFormData = {
...prev,
...changedFormData, // 변경된 필드들만 업데이트
};
console.log("📊 메인 폼 데이터 업데이트:", updatedFormData);
return updatedFormData;
});
}}
/>
</div>
</ScreenPreviewProvider>
2025-09-01 18:42:59 +09:00
);
}