화면 일괄삭제기능

This commit is contained in:
kjs
2025-12-03 16:02:09 +09:00
parent 8317af92cd
commit eb5ea411c9
11 changed files with 830 additions and 192 deletions

View File

@@ -112,6 +112,22 @@ export const screenApi = {
});
},
// 활성 화면 일괄 삭제 (휴지통으로 이동)
bulkDeleteScreens: async (
screenIds: number[],
deleteReason?: string,
force?: boolean,
): Promise<{
deletedCount: number;
skippedCount: number;
errors: Array<{ screenId: number; error: string }>;
}> => {
const response = await apiClient.delete("/screen-management/screens/bulk/delete", {
data: { screenIds, deleteReason, force },
});
return response.data.result;
},
// 휴지통 화면 목록 조회
getDeletedScreens: async (params: {
page?: number;

View File

@@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo, useCallback } from "react";
import { ComponentRendererProps } from "@/types/component";
import { CardDisplayConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
import { getFullImageUrl } from "@/lib/api/client";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
@@ -233,14 +234,17 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
return String(data.id || data.objid || data.ID || index);
}, []);
// 카드 선택 핸들러 (테이블 리스트와 동일한 로직)
// 카드 선택 핸들러 (단일 선택 - 다른 카드 선택 시 기존 선택 해제)
const handleCardSelection = useCallback((cardKey: string, data: any, checked: boolean) => {
const newSelectedRows = new Set(selectedRows);
// 단일 선택: 새로운 Set 생성 (기존 선택 초기화)
const newSelectedRows = new Set<string>();
if (checked) {
// 선택 시 해당 카드만 선택
newSelectedRows.add(cardKey);
} else {
newSelectedRows.delete(cardKey);
}
// checked가 false면 빈 Set (선택 해제)
setSelectedRows(newSelectedRows);
// 선택된 카드 데이터 계산
@@ -265,35 +269,35 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
additionalData: {},
}));
useModalDataStore.getState().setData(tableNameToUse, modalItems);
console.log("[CardDisplay] modalDataStore에 데이터 저장:", {
console.log("[CardDisplay] modalDataStore에 데이터 저장:", {
dataSourceId: tableNameToUse,
count: modalItems.length,
});
} else if (tableNameToUse && selectedRowsData.length === 0) {
useModalDataStore.getState().clearData(tableNameToUse);
console.log("🗑️ [CardDisplay] modalDataStore 데이터 제거:", tableNameToUse);
console.log("[CardDisplay] modalDataStore 데이터 제거:", tableNameToUse);
}
// 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
if (splitPanelContext && splitPanelPosition === "left") {
if (checked) {
splitPanelContext.setSelectedLeftData(data);
console.log("🔗 [CardDisplay] 분할 패널 좌측 데이터 저장:", {
console.log("[CardDisplay] 분할 패널 좌측 데이터 저장:", {
data,
parentDataMapping: splitPanelContext.parentDataMapping,
});
} else if (newSelectedRows.size === 0) {
} else {
splitPanelContext.setSelectedLeftData(null);
console.log("🔗 [CardDisplay] 분할 패널 좌측 데이터 초기화");
console.log("[CardDisplay] 분할 패널 좌측 데이터 초기화");
}
}
}, [selectedRows, displayData, getCardKey, onFormDataChange, componentConfig.dataSource?.tableName, tableName, splitPanelContext, splitPanelPosition]);
}, [displayData, getCardKey, onFormDataChange, componentConfig.dataSource?.tableName, tableName, splitPanelContext, splitPanelPosition]);
const handleCardClick = useCallback((data: any, index: number) => {
const cardKey = getCardKey(data, index);
const isCurrentlySelected = selectedRows.has(cardKey);
// 선택 토글
// 단일 선택: 이미 선택된 카드 클릭 시 선택 해제, 아니면 새로 선택
handleCardSelection(cardKey, data, !isCurrentlySelected);
if (componentConfig.onCardClick) {
@@ -396,9 +400,24 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
// 컬럼명을 라벨로 변환하는 헬퍼 함수
const getColumnLabel = (columnName: string) => {
if (!actualTableColumns || actualTableColumns.length === 0) return columnName;
const column = actualTableColumns.find((col) => col.columnName === columnName);
return column?.columnLabel || columnName;
if (!actualTableColumns || actualTableColumns.length === 0) {
// 컬럼 정보가 없으면 컬럼명을 보기 좋게 변환
return formatColumnName(columnName);
}
const column = actualTableColumns.find(
(col) => col.columnName === columnName || col.column_name === columnName
);
// 다양한 라벨 필드명 지원 (displayName이 API에서 반환하는 라벨)
const label = column?.displayName || column?.columnLabel || column?.column_label || column?.label;
return label || formatColumnName(columnName);
};
// 컬럼명을 보기 좋은 형태로 변환 (snake_case -> 공백 구분)
const formatColumnName = (columnName: string) => {
// 언더스코어를 공백으로 변환하고 각 단어 첫 글자 대문자화
return columnName
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase());
};
// 자동 폴백 로직 - 컬럼이 설정되지 않은 경우 적절한 기본값 찾기
@@ -509,9 +528,25 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
getColumnValue(data, componentConfig.columnMapping?.descriptionColumn) ||
getAutoFallbackValue(data, "description");
const imageValue = componentConfig.columnMapping?.imageColumn
? getColumnValue(data, componentConfig.columnMapping.imageColumn)
: data.avatar || data.image || "";
// 이미지 컬럼 자동 감지 (image_path, photo 등) - 대소문자 무시
const imageColumn = componentConfig.columnMapping?.imageColumn ||
Object.keys(data).find(key => {
const lowerKey = key.toLowerCase();
return lowerKey.includes('image') || lowerKey.includes('photo') ||
lowerKey.includes('avatar') || lowerKey.includes('thumbnail') ||
lowerKey.includes('picture') || lowerKey.includes('img');
});
// 이미지 값 가져오기 (직접 접근 + 폴백)
const imageValue = imageColumn
? data[imageColumn]
: (data.image_path || data.imagePath || data.avatar || data.image || data.photo || "");
// 이미지 표시 여부 결정: 이미지 값이 있거나, 설정에서 활성화된 경우
const shouldShowImage = componentConfig.cardStyle?.showImage !== false;
// 이미지 URL 생성 (TableListComponent와 동일한 로직 사용)
const imageUrl = imageValue ? getFullImageUrl(imageValue) : "";
const cardKey = getCardKey(data, index);
const isCardSelected = selectedRows.has(cardKey);
@@ -526,78 +561,94 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
boxShadow: isCardSelected
? "0 4px 6px -1px rgba(0, 0, 0, 0.15)"
: "0 1px 3px rgba(0, 0, 0, 0.08)",
flexDirection: "row", // 가로 배치
}}
className="card-hover group cursor-pointer transition-all duration-150"
onClick={() => handleCardClick(data, index)}
>
{/* 카드 이미지 */}
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
<div className="mb-2 flex justify-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<span className="text-lg text-primary">👤</span>
</div>
</div>
)}
{/* 카드 타이틀 + 서브타이틀 (가로 배치) */}
{(componentConfig.cardStyle?.showTitle || componentConfig.cardStyle?.showSubtitle) && (
<div className="mb-2 flex items-center gap-2 flex-wrap">
{componentConfig.cardStyle?.showTitle && (
<h3 className="text-base font-semibold text-foreground leading-tight">{titleValue}</h3>
)}
{componentConfig.cardStyle?.showSubtitle && subtitleValue && (
<span className="text-xs font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full">{subtitleValue}</span>
{/* 카드 이미지 - 좌측 전체 높이 (이미지 컬럼이 있으면 자동 표시) */}
{shouldShowImage && (
<div className="flex-shrink-0 flex items-center justify-center mr-4">
{imageUrl ? (
<img
src={imageUrl}
alt={titleValue || "이미지"}
className="h-16 w-16 rounded-lg object-cover border border-gray-200"
onError={(e) => {
// 이미지 로드 실패 시 기본 아이콘으로 대체
e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Crect width='64' height='64' fill='%23e0e7ff' rx='8'/%3E%3Ctext x='32' y='40' text-anchor='middle' fill='%236366f1' font-size='24'%3E👤%3C/text%3E%3C/svg%3E";
}}
/>
) : (
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary/10">
<span className="text-2xl text-primary">👤</span>
</div>
)}
</div>
)}
{/* 카드 설명 */}
{componentConfig.cardStyle?.showDescription && (
<div className="mb-2 flex-1">
<p className="text-xs text-muted-foreground leading-relaxed">
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
</p>
</div>
)}
{/* 추가 표시 컬럼들 - 가로 배치 */}
{componentConfig.columnMapping?.displayColumns &&
componentConfig.columnMapping.displayColumns.length > 0 && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 border-t border-border pt-2 text-xs">
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
const value = getColumnValue(data, columnName);
if (!value) return null;
return (
<div key={idx} className="flex items-center gap-1">
<span className="text-muted-foreground">{getColumnLabel(columnName)}:</span>
<span className="font-medium text-foreground">{value}</span>
</div>
);
})}
{/* 우측 컨텐츠 영역 */}
<div className="flex flex-col flex-1 min-w-0">
{/* 타이틀 + 서브타이틀 */}
{(componentConfig.cardStyle?.showTitle || componentConfig.cardStyle?.showSubtitle) && (
<div className="mb-1 flex items-center gap-2 flex-wrap">
{componentConfig.cardStyle?.showTitle && (
<h3 className="text-base font-semibold text-foreground leading-tight">{titleValue}</h3>
)}
{componentConfig.cardStyle?.showSubtitle && subtitleValue && (
<span className="text-xs font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full">{subtitleValue}</span>
)}
</div>
)}
{/* 카드 액션 */}
<div className="mt-2 flex justify-end space-x-2">
<button
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardView(data);
}}
>
</button>
<button
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardEdit(data);
}}
>
</button>
{/* 추가 표시 컬럼들 - 가로 배치 */}
{componentConfig.columnMapping?.displayColumns &&
componentConfig.columnMapping.displayColumns.length > 0 && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground">
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
const value = getColumnValue(data, columnName);
if (!value) return null;
return (
<div key={idx} className="flex items-center gap-1">
<span>{getColumnLabel(columnName)}:</span>
<span className="font-medium text-foreground">{value}</span>
</div>
);
})}
</div>
)}
{/* 카드 설명 */}
{componentConfig.cardStyle?.showDescription && descriptionValue && (
<div className="mt-1 flex-1">
<p className="text-xs text-muted-foreground leading-relaxed">
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
</p>
</div>
)}
{/* 카드 액션 */}
<div className="mt-2 flex justify-end space-x-2">
<button
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardView(data);
}}
>
</button>
<button
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardEdit(data);
}}
>
</button>
</div>
</div>
</div>
);