그룹이 보이지 않던 문제 수정
This commit is contained in:
parent
174acfacb7
commit
1bf28291b5
|
|
@ -376,6 +376,10 @@ export class ScreenManagementService {
|
|||
layoutData: LayoutData,
|
||||
companyCode: string
|
||||
): Promise<void> {
|
||||
console.log(`=== 레이아웃 저장 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}`);
|
||||
console.log(`컴포넌트 수: ${layoutData.components.length}`);
|
||||
|
||||
// 권한 확인
|
||||
const existingScreen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
|
|
@ -398,12 +402,22 @@ export class ScreenManagementService {
|
|||
for (const component of layoutData.components) {
|
||||
const { id, ...componentData } = component;
|
||||
|
||||
console.log(`저장 중인 컴포넌트:`, {
|
||||
id: component.id,
|
||||
type: component.type,
|
||||
position: component.position,
|
||||
size: component.size,
|
||||
parentId: component.parentId,
|
||||
title: (component as any).title,
|
||||
});
|
||||
|
||||
// Prisma JSON 필드에 맞는 타입으로 변환
|
||||
const properties: any = {
|
||||
...componentData,
|
||||
position: {
|
||||
x: component.position.x,
|
||||
y: component.position.y,
|
||||
z: component.position.z || 1, // z 값 포함
|
||||
},
|
||||
size: {
|
||||
width: component.size.width,
|
||||
|
|
@ -425,6 +439,8 @@ export class ScreenManagementService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`=== 레이아웃 저장 완료 ===`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -434,6 +450,9 @@ export class ScreenManagementService {
|
|||
screenId: number,
|
||||
companyCode: string
|
||||
): Promise<LayoutData | null> {
|
||||
console.log(`=== 레이아웃 로드 시작 ===`);
|
||||
console.log(`화면 ID: ${screenId}`);
|
||||
|
||||
// 권한 확인
|
||||
const existingScreen = await prisma.screen_definitions.findUnique({
|
||||
where: { screen_id: screenId },
|
||||
|
|
@ -452,6 +471,8 @@ export class ScreenManagementService {
|
|||
orderBy: { display_order: "asc" },
|
||||
});
|
||||
|
||||
console.log(`DB에서 조회된 레이아웃 수: ${layouts.length}`);
|
||||
|
||||
if (layouts.length === 0) {
|
||||
return {
|
||||
components: [],
|
||||
|
|
@ -461,16 +482,34 @@ export class ScreenManagementService {
|
|||
|
||||
const components: ComponentData[] = layouts.map((layout) => {
|
||||
const properties = layout.properties as any;
|
||||
return {
|
||||
const component = {
|
||||
id: layout.component_id,
|
||||
type: layout.component_type as any,
|
||||
position: { x: layout.position_x, y: layout.position_y },
|
||||
position: {
|
||||
x: layout.position_x,
|
||||
y: layout.position_y,
|
||||
z: properties?.position?.z || 1, // z 값 복원
|
||||
},
|
||||
size: { width: layout.width, height: layout.height },
|
||||
parentId: layout.parent_id,
|
||||
...properties,
|
||||
};
|
||||
|
||||
console.log(`로드된 컴포넌트:`, {
|
||||
id: component.id,
|
||||
type: component.type,
|
||||
position: component.position,
|
||||
size: component.size,
|
||||
parentId: component.parentId,
|
||||
title: (component as any).title,
|
||||
});
|
||||
|
||||
return component;
|
||||
});
|
||||
|
||||
console.log(`=== 레이아웃 로드 완료 ===`);
|
||||
console.log(`반환할 컴포넌트 수: ${components.length}`);
|
||||
|
||||
return {
|
||||
components,
|
||||
gridSettings: { columns: 12, gap: 16, padding: 16 },
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type WebType =
|
|||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
z?: number; // z-index (레이어 순서)
|
||||
}
|
||||
|
||||
// 크기 정보
|
||||
|
|
|
|||
|
|
@ -171,32 +171,90 @@ export default function ScreenViewPage() {
|
|||
{/* 실제 화면 렌더링 영역 */}
|
||||
<div className="relative h-full w-full bg-white">
|
||||
{layout.components
|
||||
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링
|
||||
.map((component) => (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={component}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
.filter((comp) => !comp.parentId) // 최상위 컴포넌트만 렌더링 (그룹 포함)
|
||||
.map((component) => {
|
||||
// 그룹 컴포넌트인 경우 특별 처리
|
||||
if (component.type === "group") {
|
||||
const groupChildren = layout.components.filter((child) => child.parentId === component.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.1)",
|
||||
border: (component as any).border || "2px dashed #3b82f6",
|
||||
borderRadius: (component as any).borderRadius || "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
{/* 그룹 제목 */}
|
||||
{(component as any).title && (
|
||||
<div className="mb-2 text-sm font-medium text-blue-700">{(component as any).title}</div>
|
||||
)}
|
||||
|
||||
{/* 그룹 내 자식 컴포넌트들 렌더링 */}
|
||||
{groupChildren.map((child) => (
|
||||
<div
|
||||
key={child.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${child.position.x}px`,
|
||||
top: `${child.position.y}px`,
|
||||
width: `${child.size.width}px`,
|
||||
height: `${child.size.height}px`,
|
||||
zIndex: child.position.z || 1,
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={child}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 일반 컴포넌트 렌더링
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${component.position.x}px`,
|
||||
top: `${component.position.y}px`,
|
||||
width: `${component.size.width}px`,
|
||||
height: `${component.size.height}px`,
|
||||
zIndex: component.position.z || 1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
<InteractiveScreenViewer
|
||||
component={component}
|
||||
allComponents={layout.components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ interface GroupingToolbarProps {
|
|||
allComponents: ComponentData[];
|
||||
onGroupAlign?: (mode: "left" | "centerX" | "right" | "top" | "centerY" | "bottom") => void;
|
||||
onGroupDistribute?: (orientation: "horizontal" | "vertical") => void;
|
||||
showCreateDialog?: boolean;
|
||||
onShowCreateDialogChange?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
|
||||
|
|
@ -50,8 +52,12 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
|
|||
allComponents,
|
||||
onGroupAlign,
|
||||
onGroupDistribute,
|
||||
showCreateDialog: externalShowCreateDialog,
|
||||
onShowCreateDialogChange,
|
||||
}) => {
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [internalShowCreateDialog, setInternalShowCreateDialog] = useState(false);
|
||||
const showCreateDialog = externalShowCreateDialog ?? internalShowCreateDialog;
|
||||
const setShowCreateDialog = onShowCreateDialogChange ?? setInternalShowCreateDialog;
|
||||
const [groupTitle, setGroupTitle] = useState("새 그룹");
|
||||
const [groupStyle, setGroupStyle] = useState<ComponentStyle>(createGroupStyle());
|
||||
|
||||
|
|
@ -131,10 +137,13 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
|
|||
size="sm"
|
||||
onClick={handleCreateGroup}
|
||||
disabled={!canCreateGroup}
|
||||
title={canCreateGroup ? "선택된 컴포넌트를 그룹으로 묶기" : "2개 이상의 컴포넌트를 선택하세요"}
|
||||
title={canCreateGroup ? "선택된 컴포넌트를 그룹으로 묶기 (Ctrl+G)" : "2개 이상의 컴포넌트를 선택하세요"}
|
||||
>
|
||||
<Group className="mr-1 h-3 w-3" />
|
||||
그룹 생성
|
||||
<kbd className="bg-muted text-muted-foreground ml-2 hidden rounded px-1.5 py-0.5 font-mono text-xs sm:inline-block">
|
||||
Ctrl+G
|
||||
</kbd>
|
||||
</Button>
|
||||
|
||||
{/* 그룹 해제 버튼 */}
|
||||
|
|
@ -143,10 +152,13 @@ export const GroupingToolbar: React.FC<GroupingToolbarProps> = ({
|
|||
size="sm"
|
||||
onClick={handleUngroup}
|
||||
disabled={!selectedGroup}
|
||||
title={selectedGroup ? "선택된 그룹 해제" : "그룹을 선택하세요"}
|
||||
title={selectedGroup ? "선택된 그룹 해제 (Ctrl+Shift+G)" : "그룹을 선택하세요"}
|
||||
>
|
||||
<Ungroup className="mr-1 h-3 w-3" />
|
||||
그룹 해제
|
||||
<kbd className="bg-muted text-muted-foreground ml-2 hidden rounded px-1.5 py-0.5 font-mono text-xs sm:inline-block">
|
||||
Ctrl+⇧+G
|
||||
</kbd>
|
||||
</Button>
|
||||
|
||||
{/* 선택 해제 버튼 */}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,9 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
groupMode: "create",
|
||||
});
|
||||
|
||||
// 그룹 생성 다이얼로그 상태
|
||||
const [showGroupCreateDialog, setShowGroupCreateDialog] = useState(false);
|
||||
|
||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
||||
|
||||
|
|
@ -766,6 +769,57 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
pasteComponents();
|
||||
}
|
||||
break;
|
||||
case "g":
|
||||
case "G":
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
// Ctrl+Shift+G: 그룹 해제
|
||||
const selectedComponents = layout.components.filter((comp) =>
|
||||
groupState.selectedComponents.includes(comp.id),
|
||||
);
|
||||
if (selectedComponents.length === 1 && selectedComponents[0].type === "group") {
|
||||
// 그룹 해제 로직을 직접 실행
|
||||
const group = selectedComponents[0] as any;
|
||||
const groupChildren = layout.components.filter((comp) => comp.parentId === group.id);
|
||||
|
||||
// 자식 컴포넌트들의 절대 위치 복원
|
||||
const absoluteChildren = groupChildren.map((child) => ({
|
||||
...child,
|
||||
position: {
|
||||
...child.position,
|
||||
x: child.position.x + group.position.x,
|
||||
y: child.position.y + group.position.y,
|
||||
z: child.position.z || 1,
|
||||
},
|
||||
parentId: undefined,
|
||||
}));
|
||||
|
||||
const newLayout = {
|
||||
...layout,
|
||||
components: [
|
||||
...layout.components.filter((comp) => comp.id !== group.id && comp.parentId !== group.id),
|
||||
...absoluteChildren,
|
||||
],
|
||||
};
|
||||
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
setGroupState((prev) => ({
|
||||
...prev,
|
||||
selectedComponents: [],
|
||||
isGrouping: false,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Ctrl+G: 그룹 생성 다이얼로그 열기
|
||||
const selectedComponents = layout.components.filter((comp) =>
|
||||
groupState.selectedComponents.includes(comp.id),
|
||||
);
|
||||
if (selectedComponents.length >= 2) {
|
||||
setShowGroupCreateDialog(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (e.key === "Delete") {
|
||||
e.preventDefault();
|
||||
|
|
@ -776,7 +830,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [undo, redo, copyComponents, pasteComponents, deleteComponents, clipboard]);
|
||||
}, [
|
||||
undo,
|
||||
redo,
|
||||
copyComponents,
|
||||
pasteComponents,
|
||||
deleteComponents,
|
||||
clipboard,
|
||||
layout,
|
||||
groupState,
|
||||
saveToHistory,
|
||||
setLayout,
|
||||
setGroupState,
|
||||
setShowGroupCreateDialog,
|
||||
]);
|
||||
|
||||
// 컴포넌트 속성 업데이트 함수
|
||||
const updateComponentProperty = useCallback(
|
||||
|
|
@ -1184,14 +1251,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
const handleComponentClick = useCallback(
|
||||
(component: ComponentData, event?: React.MouseEvent) => {
|
||||
const isShiftPressed = event?.shiftKey || false;
|
||||
|
||||
// 그룹 컨테이너는 다중선택 대상에서 제외
|
||||
const isGroupContainer = component.type === "group";
|
||||
|
||||
if (groupState.isGrouping || isShiftPressed) {
|
||||
// 그룹화 모드이거나 시프트 키를 누른 경우 다중 선택
|
||||
if (isGroupContainer) {
|
||||
// 그룹 컨테이너 클릭은 다중선택에 포함하지 않고 무시
|
||||
// 그룹 컨테이너는 다중선택에서 제외하고 단일 선택으로 처리
|
||||
setSelectedComponent(component);
|
||||
setGroupState((prev) => ({
|
||||
...prev,
|
||||
selectedComponents: [component.id],
|
||||
isGrouping: false, // 그룹 선택 시 그룹화 모드 해제
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const isSelected = groupState.selectedComponents.includes(component.id);
|
||||
|
|
@ -1211,7 +1282,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
setSelectedComponent(component);
|
||||
setGroupState((prev) => ({
|
||||
...prev,
|
||||
selectedComponents: isGroupContainer ? [] : [component.id],
|
||||
selectedComponents: [component.id], // 그룹도 선택 가능하도록 수정
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
|
@ -1392,6 +1463,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
}}
|
||||
showCreateDialog={showGroupCreateDialog}
|
||||
onShowCreateDialogChange={setShowGroupCreateDialog}
|
||||
/>
|
||||
|
||||
{/* 메인 컨텐츠 영역 */}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export function createGroupComponent(
|
|||
return {
|
||||
id: generateComponentId(),
|
||||
type: "group",
|
||||
position,
|
||||
position: { ...position, z: position.z || 1 }, // z 값 포함
|
||||
size: { width: groupWidth, height: groupHeight },
|
||||
label: title, // title 대신 label 사용
|
||||
title: title, // GroupComponent 타입에 맞게 title 사용
|
||||
backgroundColor: "#f8f9fa",
|
||||
border: "1px solid #dee2e6",
|
||||
borderRadius: 8,
|
||||
|
|
@ -78,6 +78,7 @@ export function calculateRelativePositions(
|
|||
position: {
|
||||
x: component.position.x - groupPosition.x,
|
||||
y: component.position.y - groupPosition.y,
|
||||
z: component.position.z || 1, // z 값 유지
|
||||
},
|
||||
parentId: groupId, // 그룹 ID를 부모로 설정
|
||||
}));
|
||||
|
|
@ -90,6 +91,7 @@ export function restoreAbsolutePositions(components: ComponentData[], groupPosit
|
|||
position: {
|
||||
x: component.position.x + groupPosition.x,
|
||||
y: component.position.y + groupPosition.y,
|
||||
z: component.position.z || 1, // z 값 유지
|
||||
},
|
||||
parentId: undefined,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export type WebType =
|
|||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
z?: number; // z-index (레이어 순서)
|
||||
}
|
||||
|
||||
// 크기 정보
|
||||
|
|
|
|||
Loading…
Reference in New Issue