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