화면 바로 들어가지게 함
This commit is contained in:
@@ -15,6 +15,7 @@ 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";
|
||||
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
export default function ScreenViewPage() {
|
||||
const params = useParams();
|
||||
@@ -211,302 +212,305 @@ export default function ScreenViewPage() {
|
||||
const screenHeight = layout?.screenResolution?.height || 800;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
|
||||
{/* 절대 위치 기반 렌더링 */}
|
||||
{layout && layout.components.length > 0 ? (
|
||||
<div
|
||||
className="bg-background relative origin-top-left"
|
||||
style={{
|
||||
width: layout?.screenResolution?.width || 1200,
|
||||
height: layout?.screenResolution?.height || 800,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* 최상위 컴포넌트들 렌더링 */}
|
||||
{(() => {
|
||||
// 🆕 플로우 버튼 그룹 감지 및 처리
|
||||
const topLevelComponents = layout.components.filter((component) => !component.parentId);
|
||||
<ScreenPreviewProvider isPreviewMode={false}>
|
||||
<div ref={containerRef} className="bg-background flex h-full w-full items-start justify-start overflow-hidden">
|
||||
{/* 절대 위치 기반 렌더링 */}
|
||||
{layout && layout.components.length > 0 ? (
|
||||
<div
|
||||
className="bg-background relative origin-top-left"
|
||||
style={{
|
||||
width: layout?.screenResolution?.width || 1200,
|
||||
height: layout?.screenResolution?.height || 800,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* 최상위 컴포넌트들 렌더링 */}
|
||||
{(() => {
|
||||
// 🆕 플로우 버튼 그룹 감지 및 처리
|
||||
const topLevelComponents = layout.components.filter((component) => !component.parentId);
|
||||
|
||||
const buttonGroups: Record<string, any[]> = {};
|
||||
const processedButtonIds = new Set<string>();
|
||||
const buttonGroups: Record<string, any[]> = {};
|
||||
const processedButtonIds = new Set<string>();
|
||||
|
||||
topLevelComponents.forEach((component) => {
|
||||
const isButton =
|
||||
component.type === "button" ||
|
||||
(component.type === "component" &&
|
||||
["button-primary", "button-secondary"].includes((component as any).componentType));
|
||||
topLevelComponents.forEach((component) => {
|
||||
const isButton =
|
||||
component.type === "button" ||
|
||||
(component.type === "component" &&
|
||||
["button-primary", "button-secondary"].includes((component as any).componentType));
|
||||
|
||||
if (isButton) {
|
||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
|
||||
| FlowVisibilityConfig
|
||||
| undefined;
|
||||
if (isButton) {
|
||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig as
|
||||
| FlowVisibilityConfig
|
||||
| undefined;
|
||||
|
||||
if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) {
|
||||
if (!buttonGroups[flowConfig.groupId]) {
|
||||
buttonGroups[flowConfig.groupId] = [];
|
||||
if (flowConfig?.enabled && flowConfig.layoutBehavior === "auto-compact" && flowConfig.groupId) {
|
||||
if (!buttonGroups[flowConfig.groupId]) {
|
||||
buttonGroups[flowConfig.groupId] = [];
|
||||
}
|
||||
buttonGroups[flowConfig.groupId].push(component);
|
||||
processedButtonIds.add(component.id);
|
||||
}
|
||||
buttonGroups[flowConfig.groupId].push(component);
|
||||
processedButtonIds.add(component.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
||||
const regularComponents = topLevelComponents.filter((c) => !processedButtonIds.has(c.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 일반 컴포넌트들 */}
|
||||
{regularComponents.map((component) => (
|
||||
<RealtimePreview
|
||||
key={component.id}
|
||||
component={component}
|
||||
isSelected={false}
|
||||
isDesignMode={false}
|
||||
onClick={() => {}}
|
||||
screenId={screenId}
|
||||
tableName={screen?.tableName}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(_, selectedData) => {
|
||||
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
||||
setSelectedRowsData(selectedData);
|
||||
}}
|
||||
flowSelectedData={flowSelectedData}
|
||||
flowSelectedStepId={flowSelectedStepId}
|
||||
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
||||
console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", {
|
||||
dataCount: selectedData.length,
|
||||
selectedData,
|
||||
stepId,
|
||||
});
|
||||
setFlowSelectedData(selectedData);
|
||||
setFlowSelectedStepId(stepId);
|
||||
console.log("🔍 [page.tsx] 상태 업데이트 완료");
|
||||
}}
|
||||
refreshKey={tableRefreshKey}
|
||||
onRefresh={() => {
|
||||
console.log("🔄 테이블 새로고침 요청됨");
|
||||
setTableRefreshKey((prev) => prev + 1);
|
||||
setSelectedRowsData([]); // 선택 해제
|
||||
}}
|
||||
flowRefreshKey={flowRefreshKey}
|
||||
onFlowRefresh={() => {
|
||||
console.log("🔄 플로우 새로고침 요청됨");
|
||||
setFlowRefreshKey((prev) => prev + 1);
|
||||
setFlowSelectedData([]); // 선택 해제
|
||||
setFlowSelectedStepId(null);
|
||||
}}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("📝 폼 데이터 변경:", 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}
|
||||
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 }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RealtimePreview>
|
||||
))}
|
||||
|
||||
{/* 🆕 플로우 버튼 그룹들 */}
|
||||
{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;
|
||||
|
||||
// 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용
|
||||
const groupPosition = buttons.reduce(
|
||||
(min, button) => ({
|
||||
x: Math.min(min.x, button.position.x),
|
||||
y: Math.min(min.y, button.position.y),
|
||||
z: min.z,
|
||||
}),
|
||||
{ x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 },
|
||||
);
|
||||
|
||||
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
|
||||
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",
|
||||
left: `${groupPosition.x}px`,
|
||||
top: `${groupPosition.y}px`,
|
||||
zIndex: groupPosition.z,
|
||||
width: `${groupWidth}px`,
|
||||
height: `${groupHeight}px`,
|
||||
return (
|
||||
<>
|
||||
{/* 일반 컴포넌트들 */}
|
||||
{regularComponents.map((component) => (
|
||||
<RealtimePreview
|
||||
key={component.id}
|
||||
component={component}
|
||||
isSelected={false}
|
||||
isDesignMode={false}
|
||||
onClick={() => {}}
|
||||
screenId={screenId}
|
||||
tableName={screen?.tableName}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(_, selectedData) => {
|
||||
console.log("🔍 화면에서 선택된 행 데이터:", selectedData);
|
||||
setSelectedRowsData(selectedData);
|
||||
}}
|
||||
flowSelectedData={flowSelectedData}
|
||||
flowSelectedStepId={flowSelectedStepId}
|
||||
onFlowSelectedDataChange={(selectedData: any[], stepId: number | null) => {
|
||||
console.log("🔍 [page.tsx] 플로우 선택된 데이터 받음:", {
|
||||
dataCount: selectedData.length,
|
||||
selectedData,
|
||||
stepId,
|
||||
});
|
||||
setFlowSelectedData(selectedData);
|
||||
setFlowSelectedStepId(stepId);
|
||||
console.log("🔍 [page.tsx] 상태 업데이트 완료");
|
||||
}}
|
||||
refreshKey={tableRefreshKey}
|
||||
onRefresh={() => {
|
||||
console.log("🔄 테이블 새로고침 요청됨");
|
||||
setTableRefreshKey((prev) => prev + 1);
|
||||
setSelectedRowsData([]); // 선택 해제
|
||||
}}
|
||||
flowRefreshKey={flowRefreshKey}
|
||||
onFlowRefresh={() => {
|
||||
console.log("🔄 플로우 새로고침 요청됨");
|
||||
setFlowRefreshKey((prev) => prev + 1);
|
||||
setFlowSelectedData([]); // 선택 해제
|
||||
setFlowSelectedStepId(null);
|
||||
}}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
console.log("📝 폼 데이터 변경:", fieldName, "=", value);
|
||||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||
}}
|
||||
>
|
||||
<FlowButtonGroup
|
||||
buttons={buttons}
|
||||
groupConfig={groupConfig}
|
||||
isDesignMode={false}
|
||||
renderButton={(button) => {
|
||||
const relativeButton = {
|
||||
...button,
|
||||
position: { x: 0, y: 0, z: button.position.z || 1 },
|
||||
};
|
||||
{/* 자식 컴포넌트들 */}
|
||||
{(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 (
|
||||
<div
|
||||
key={button.id}
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
width: button.size?.width || 100,
|
||||
height: button.size?.height || 40,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<DynamicComponentRenderer
|
||||
component={relativeButton}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={formData}
|
||||
onDataflowComplete={() => {}}
|
||||
screenId={screenId}
|
||||
tableName={screen?.tableName}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(_, 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);
|
||||
}}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<RealtimePreview
|
||||
key={child.id}
|
||||
component={relativeChildComponent}
|
||||
isSelected={false}
|
||||
isDesignMode={false}
|
||||
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 }));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RealtimePreview>
|
||||
))}
|
||||
|
||||
{/* 🆕 플로우 버튼 그룹들 */}
|
||||
{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;
|
||||
|
||||
// 그룹의 위치는 모든 버튼 중 가장 왼쪽/위쪽 버튼의 위치 사용
|
||||
const groupPosition = buttons.reduce(
|
||||
(min, button) => ({
|
||||
x: Math.min(min.x, button.position.x),
|
||||
y: Math.min(min.y, button.position.y),
|
||||
z: min.z,
|
||||
}),
|
||||
{ x: buttons[0].position.x, y: buttons[0].position.y, z: buttons[0].position.z || 2 },
|
||||
);
|
||||
|
||||
// 그룹의 크기 계산: 버튼들의 실제 크기 + 간격을 기준으로 계산
|
||||
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",
|
||||
left: `${groupPosition.x}px`,
|
||||
top: `${groupPosition.y}px`,
|
||||
zIndex: groupPosition.z,
|
||||
width: `${groupWidth}px`,
|
||||
height: `${groupHeight}px`,
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<FlowButtonGroup
|
||||
buttons={buttons}
|
||||
groupConfig={groupConfig}
|
||||
isDesignMode={false}
|
||||
renderButton={(button) => {
|
||||
const relativeButton = {
|
||||
...button,
|
||||
position: { x: 0, y: 0, z: button.position.z || 1 },
|
||||
};
|
||||
|
||||
{/* 편집 모달 */}
|
||||
<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>
|
||||
return (
|
||||
<div
|
||||
key={button.id}
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
width: button.size?.width || 100,
|
||||
height: button.size?.height || 40,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<DynamicComponentRenderer
|
||||
component={relativeButton}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={formData}
|
||||
onDataflowComplete={() => {}}
|
||||
screenId={screenId}
|
||||
tableName={screen?.tableName}
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(_, 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);
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 편집 모달 */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import { toast } from "sonner";
|
||||
import { FileUpload } from "@/components/screen/widgets/FileUpload";
|
||||
import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters";
|
||||
import { SaveModal } from "./SaveModal";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
// 파일 데이터 타입 정의 (AttachedFileInfo와 호환)
|
||||
interface FileInfo {
|
||||
@@ -97,6 +98,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
style = {},
|
||||
onRefresh,
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
||||
@@ -411,6 +413,29 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
async (page: number = 1, searchParams: Record<string, any> = {}) => {
|
||||
if (!component.tableName) return;
|
||||
|
||||
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||
if (isPreviewMode) {
|
||||
const sampleData = Array.from({ length: 3 }, (_, i) => {
|
||||
const sample: Record<string, any> = { id: i + 1 };
|
||||
component.columns.forEach((col) => {
|
||||
if (col.type === "number") {
|
||||
sample[col.key] = Math.floor(Math.random() * 1000);
|
||||
} else if (col.type === "boolean") {
|
||||
sample[col.key] = i % 2 === 0 ? "Y" : "N";
|
||||
} else {
|
||||
sample[col.key] = `샘플 ${col.label} ${i + 1}`;
|
||||
}
|
||||
});
|
||||
return sample;
|
||||
});
|
||||
setData(sampleData);
|
||||
setTotal(3);
|
||||
setTotalPages(1);
|
||||
setCurrentPage(1);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||
@@ -1792,21 +1817,53 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
{/* CRUD 버튼들 */}
|
||||
{component.enableAdd && (
|
||||
<Button size="sm" onClick={handleAddData} disabled={loading} className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
handleAddData();
|
||||
}}
|
||||
disabled={loading || isPreviewMode}
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
{component.addButtonText || "추가"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{component.enableEdit && selectedRows.size === 1 && (
|
||||
<Button size="sm" onClick={handleEditData} disabled={loading} className="gap-2" variant="outline">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
handleEditData();
|
||||
}}
|
||||
disabled={loading || isPreviewMode}
|
||||
className="gap-2"
|
||||
variant="outline"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
{component.editButtonText || "수정"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{component.enableDelete && selectedRows.size > 0 && (
|
||||
<Button size="sm" variant="destructive" onClick={handleDeleteData} disabled={loading} className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
handleDeleteData();
|
||||
}}
|
||||
disabled={loading || isPreviewMode}
|
||||
className="gap-2"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
{component.deleteButtonText || "삭제"}
|
||||
</Button>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { UnifiedColumnInfo as ColumnInfo } from "@/types";
|
||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||
import { buildGridClasses } from "@/lib/constants/columnSpans";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
interface InteractiveScreenViewerProps {
|
||||
component: ComponentData;
|
||||
@@ -86,6 +87,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
return <div className="h-full w-full" />;
|
||||
}
|
||||
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const { userName, user } = useAuth(); // 현재 로그인한 사용자명과 사용자 정보 가져오기
|
||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||
@@ -211,6 +213,11 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 폼 데이터 업데이트
|
||||
const updateFormData = (fieldName: string, value: any) => {
|
||||
// 프리뷰 모드에서는 데이터 업데이트 하지 않음
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`🔄 updateFormData: ${fieldName} = "${value}" (외부콜백: ${!!onFormDataChange})`);
|
||||
|
||||
// 항상 로컬 상태도 업데이트
|
||||
@@ -837,6 +844,12 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
});
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 프리뷰 모드에서는 파일 업로드 차단
|
||||
if (isPreviewMode) {
|
||||
e.target.value = ""; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
const files = e.target.files;
|
||||
const fieldName = widget.columnName || widget.id;
|
||||
|
||||
@@ -1155,6 +1168,11 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
// 프리뷰 모드에서는 버튼 동작 차단
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionType = config?.actionType || "save";
|
||||
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/
|
||||
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
|
||||
import "@/lib/registry/components/ButtonRenderer";
|
||||
@@ -47,6 +48,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
hideLabel = false,
|
||||
screenInfo,
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const { userName, user } = useAuth();
|
||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||
@@ -405,7 +407,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
await handleCustomAction();
|
||||
break;
|
||||
default:
|
||||
// console.log("🔘 기본 버튼 클릭");
|
||||
// console.log("🔘 기본 버튼 클릭");
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("버튼 액션 오류:", error);
|
||||
@@ -437,9 +439,10 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const fieldName = comp.columnName || comp.id;
|
||||
|
||||
// 화면 ID 추출 (URL에서)
|
||||
const screenId = screenInfo?.screenId ||
|
||||
(typeof window !== 'undefined' && window.location.pathname.includes('/screens/')
|
||||
? parseInt(window.location.pathname.split('/screens/')[1])
|
||||
const screenId =
|
||||
screenInfo?.screenId ||
|
||||
(typeof window !== "undefined" && window.location.pathname.includes("/screens/")
|
||||
? parseInt(window.location.pathname.split("/screens/")[1])
|
||||
: null);
|
||||
|
||||
return (
|
||||
@@ -455,8 +458,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
disabled: readonly,
|
||||
}}
|
||||
componentStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
className="h-full w-full"
|
||||
isInteractive={true}
|
||||
@@ -465,12 +468,12 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
screenId, // 🎯 화면 ID 전달
|
||||
// 🎯 백엔드 API가 기대하는 정확한 형식으로 설정
|
||||
autoLink: true, // 자동 연결 활성화
|
||||
linkedTable: 'screen_files', // 연결 테이블
|
||||
linkedTable: "screen_files", // 연결 테이블
|
||||
recordId: screenId, // 레코드 ID
|
||||
columnName: fieldName, // 컬럼명 (중요!)
|
||||
isVirtualFileColumn: true, // 가상 파일 컬럼
|
||||
id: formData.id,
|
||||
...formData
|
||||
...formData,
|
||||
}}
|
||||
onFormDataChange={(data) => {
|
||||
// console.log("📝 실제 화면 파일 업로드 완료:", data);
|
||||
@@ -486,50 +489,54 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
hasUploadedFiles: !!updates.uploadedFiles,
|
||||
filesCount: updates.uploadedFiles?.length || 0,
|
||||
hasLastFileUpdate: !!updates.lastFileUpdate,
|
||||
updates
|
||||
updates,
|
||||
});
|
||||
|
||||
|
||||
// 파일 업로드/삭제 완료 시 formData 업데이터
|
||||
if (updates.uploadedFiles && onFormDataChange) {
|
||||
onFormDataChange(fieldName, updates.uploadedFiles);
|
||||
}
|
||||
|
||||
|
||||
// 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두)
|
||||
if (updates.uploadedFiles !== undefined && typeof window !== 'undefined') {
|
||||
if (updates.uploadedFiles !== undefined && typeof window !== "undefined") {
|
||||
// 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음)
|
||||
const action = updates.lastFileUpdate ? 'update' : 'sync';
|
||||
|
||||
const action = updates.lastFileUpdate ? "update" : "sync";
|
||||
|
||||
const eventDetail = {
|
||||
componentId: comp.id,
|
||||
files: updates.uploadedFiles,
|
||||
fileCount: updates.uploadedFiles.length,
|
||||
action: action,
|
||||
timestamp: updates.lastFileUpdate || Date.now(),
|
||||
source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시
|
||||
source: "realScreen", // 실제 화면에서 온 이벤트임을 표시
|
||||
};
|
||||
|
||||
|
||||
// console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail);
|
||||
|
||||
const event = new CustomEvent('globalFileStateChanged', {
|
||||
detail: eventDetail
|
||||
|
||||
const event = new CustomEvent("globalFileStateChanged", {
|
||||
detail: eventDetail,
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
|
||||
// console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료");
|
||||
|
||||
|
||||
// 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비)
|
||||
setTimeout(() => {
|
||||
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)");
|
||||
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
||||
detail: { ...eventDetail, delayed: true }
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("globalFileStateChanged", {
|
||||
detail: { ...eventDetail, delayed: true },
|
||||
}),
|
||||
);
|
||||
}, 100);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
// console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)");
|
||||
window.dispatchEvent(new CustomEvent('globalFileStateChanged', {
|
||||
detail: { ...eventDetail, delayed: true, attempt: 2 }
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("globalFileStateChanged", {
|
||||
detail: { ...eventDetail, delayed: true, attempt: 2 },
|
||||
}),
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -448,10 +448,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
{screens.map((screen) => (
|
||||
<TableRow
|
||||
key={screen.screenId}
|
||||
className={`hover:bg-muted/50 border-b transition-colors ${
|
||||
className={`hover:bg-muted/50 cursor-pointer border-b transition-colors ${
|
||||
selectedScreen?.screenId === screen.screenId ? "border-primary/20 bg-accent" : ""
|
||||
}`}
|
||||
onClick={() => handleScreenSelect(screen)}
|
||||
onClick={() => onDesignScreen(screen)}
|
||||
>
|
||||
<TableCell className="h-16 cursor-pointer">
|
||||
<div>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
interface FlowWidgetProps {
|
||||
component: FlowComponent;
|
||||
@@ -53,6 +54,8 @@ export function FlowWidget({
|
||||
flowRefreshKey,
|
||||
onFlowRefresh,
|
||||
}: FlowWidgetProps) {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
|
||||
// 🆕 전역 상태 관리
|
||||
const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep);
|
||||
const resetFlow = useFlowStepStore((state) => state.resetFlow);
|
||||
@@ -312,6 +315,57 @@ export function FlowWidget({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||
if (isPreviewMode) {
|
||||
console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시");
|
||||
setFlowData({
|
||||
id: flowId || 0,
|
||||
flowName: flowName || "샘플 플로우",
|
||||
description: "프리뷰 모드 샘플",
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as FlowDefinition);
|
||||
|
||||
const sampleSteps: FlowStep[] = [
|
||||
{
|
||||
id: 1,
|
||||
flowId: flowId || 0,
|
||||
stepName: "시작 단계",
|
||||
stepOrder: 1,
|
||||
stepType: "start",
|
||||
stepConfig: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
flowId: flowId || 0,
|
||||
stepName: "진행 중",
|
||||
stepOrder: 2,
|
||||
stepType: "process",
|
||||
stepConfig: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
flowId: flowId || 0,
|
||||
stepName: "완료",
|
||||
stepOrder: 3,
|
||||
stepType: "end",
|
||||
stepConfig: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
setSteps(sampleSteps);
|
||||
setStepCounts({ 1: 5, 2: 3, 3: 2 });
|
||||
setConnections([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 플로우 정보 조회
|
||||
const flowResponse = await getFlowById(flowId!);
|
||||
if (!flowResponse.success || !flowResponse.data) {
|
||||
@@ -413,6 +467,11 @@ export function FlowWidget({
|
||||
|
||||
// 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가)
|
||||
const handleStepClick = async (stepId: number, stepName: string) => {
|
||||
// 프리뷰 모드에서는 스텝 클릭 차단
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 외부 콜백 실행
|
||||
if (onStepClick) {
|
||||
onStepClick(stepId, stepName);
|
||||
@@ -485,6 +544,11 @@ export function FlowWidget({
|
||||
|
||||
// 체크박스 토글
|
||||
const toggleRowSelection = (rowIndex: number) => {
|
||||
// 프리뷰 모드에서는 행 선택 차단
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelected = new Set(selectedRows);
|
||||
if (newSelected.has(rowIndex)) {
|
||||
newSelected.delete(rowIndex);
|
||||
@@ -675,7 +739,13 @@ export function FlowWidget({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsFilterSettingOpen(true)}
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
setIsFilterSettingOpen(true);
|
||||
}}
|
||||
disabled={isPreviewMode}
|
||||
className="h-8 shrink-0 text-xs sm:text-sm"
|
||||
>
|
||||
<Filter className="mr-2 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
@@ -887,17 +957,29 @@ export function FlowWidget({
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => setStepDataPage((p) => Math.max(1, p - 1))}
|
||||
className={stepDataPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
setStepDataPage((p) => Math.max(1, p - 1));
|
||||
}}
|
||||
className={
|
||||
stepDataPage === 1 || isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{totalStepDataPages <= 7 ? (
|
||||
Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
onClick={() => setStepDataPage(page)}
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
setStepDataPage(page);
|
||||
}}
|
||||
isActive={stepDataPage === page}
|
||||
className="cursor-pointer"
|
||||
className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
@@ -922,9 +1004,14 @@ export function FlowWidget({
|
||||
)}
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => setStepDataPage(page)}
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
setStepDataPage(page);
|
||||
}}
|
||||
isActive={stepDataPage === page}
|
||||
className="cursor-pointer"
|
||||
className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
@@ -935,9 +1022,16 @@ export function FlowWidget({
|
||||
)}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => setStepDataPage((p) => Math.min(totalStepDataPages, p + 1))}
|
||||
onClick={() => {
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
setStepDataPage((p) => Math.min(totalStepDataPages, p + 1));
|
||||
}}
|
||||
className={
|
||||
stepDataPage === totalStepDataPages ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||
stepDataPage === totalStepDataPages || isPreviewMode
|
||||
? "pointer-events-none opacity-50"
|
||||
: "cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
24
frontend/contexts/ScreenPreviewContext.tsx
Normal file
24
frontend/contexts/ScreenPreviewContext.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
interface ScreenPreviewContextType {
|
||||
isPreviewMode: boolean; // true: 화면 관리(디자이너), false: 실제 화면
|
||||
}
|
||||
|
||||
const ScreenPreviewContext = createContext<ScreenPreviewContextType>({
|
||||
isPreviewMode: false,
|
||||
});
|
||||
|
||||
export const useScreenPreview = () => {
|
||||
return useContext(ScreenPreviewContext);
|
||||
};
|
||||
|
||||
interface ScreenPreviewProviderProps {
|
||||
isPreviewMode: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ScreenPreviewProvider: React.FC<ScreenPreviewProviderProps> = ({ isPreviewMode, children }) => {
|
||||
return <ScreenPreviewContext.Provider value={{ isPreviewMode }}>{children}</ScreenPreviewContext.Provider>;
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
@@ -73,6 +74,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
flowSelectedStepId,
|
||||
...props
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
|
||||
// 🆕 플로우 단계별 표시 제어
|
||||
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
||||
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
||||
@@ -355,6 +358,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// 프리뷰 모드에서는 버튼 동작 차단
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 디자인 모드에서는 기본 onClick만 실행
|
||||
if (isDesignMode) {
|
||||
onClick?.();
|
||||
|
||||
Reference in New Issue
Block a user