화면 일괄삭제기능
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user