그룹이 보이지 않던 문제 수정

This commit is contained in:
kjs 2025-09-02 10:33:41 +09:00
parent 174acfacb7
commit 1bf28291b5
7 changed files with 223 additions and 37 deletions

View File

@ -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 },

View File

@ -20,6 +20,7 @@ export type WebType =
export interface Position {
x: number;
y: number;
z?: number; // z-index (레이어 순서)
}
// 크기 정보

View File

@ -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>

View File

@ -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>
{/* 선택 해제 버튼 */}

View File

@ -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}
/>
{/* 메인 컨텐츠 영역 */}

View File

@ -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,
}));

View File

@ -27,6 +27,7 @@ export type WebType =
export interface Position {
x: number;
y: number;
z?: number; // z-index (레이어 순서)
}
// 크기 정보