화면관리의 테이블 리스트, 카드 디스플레이 수정
This commit is contained in:
@@ -4,6 +4,10 @@ import React, { useEffect, useState, useMemo } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { CardDisplayConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export interface CardDisplayComponentProps extends ComponentRendererProps {
|
||||
config?: CardDisplayConfig;
|
||||
@@ -39,6 +43,59 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 상세보기 모달 상태
|
||||
const [viewModalOpen, setViewModalOpen] = useState(false);
|
||||
const [selectedData, setSelectedData] = useState<any>(null);
|
||||
|
||||
// 편집 모달 상태
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editData, setEditData] = useState<any>(null);
|
||||
|
||||
// 카드 액션 핸들러
|
||||
const handleCardView = (data: any) => {
|
||||
// console.log("👀 상세보기 클릭:", data);
|
||||
setSelectedData(data);
|
||||
setViewModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCardEdit = (data: any) => {
|
||||
// console.log("✏️ 편집 클릭:", data);
|
||||
setEditData({ ...data }); // 복사본 생성
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
// 편집 폼 데이터 변경 핸들러
|
||||
const handleEditFormChange = (key: string, value: string) => {
|
||||
setEditData((prev: any) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// 편집 저장 핸들러
|
||||
const handleEditSave = async () => {
|
||||
// console.log("💾 편집 저장:", editData);
|
||||
|
||||
try {
|
||||
// TODO: 실제 API 호출로 데이터 업데이트
|
||||
// await tableTypeApi.updateTableData(tableName, editData);
|
||||
|
||||
// console.log("✅ 편집 저장 완료");
|
||||
alert("✅ 저장되었습니다!");
|
||||
|
||||
// 모달 닫기
|
||||
setEditModalOpen(false);
|
||||
setEditData(null);
|
||||
|
||||
// 데이터 새로고침 (필요시)
|
||||
// loadTableData();
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ 편집 저장 실패:", error);
|
||||
alert("❌ 저장에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 데이터 로딩
|
||||
useEffect(() => {
|
||||
const loadTableData = async () => {
|
||||
@@ -48,19 +105,25 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
}
|
||||
|
||||
// tableName 확인 (props에서 전달받은 tableName 사용)
|
||||
const tableNameToUse = tableName || component.componentConfig?.tableName;
|
||||
const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정
|
||||
|
||||
if (!tableNameToUse) {
|
||||
console.log("📋 CardDisplay: 테이블명이 설정되지 않음", {
|
||||
tableName,
|
||||
componentTableName: component.componentConfig?.tableName,
|
||||
});
|
||||
// console.log("📋 CardDisplay: 테이블명이 설정되지 않음", {
|
||||
// tableName,
|
||||
// componentTableName: component.componentConfig?.tableName,
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("📋 CardDisplay: 사용할 테이블명", {
|
||||
// tableName,
|
||||
// componentTableName: component.componentConfig?.tableName,
|
||||
// finalTableName: tableNameToUse,
|
||||
// });
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`);
|
||||
// console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`);
|
||||
|
||||
// 테이블 데이터와 컬럼 정보를 병렬로 로드
|
||||
const [dataResponse, columnsResponse] = await Promise.all([
|
||||
@@ -71,13 +134,13 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
tableTypeApi.getColumns(tableNameToUse),
|
||||
]);
|
||||
|
||||
console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, {
|
||||
total: dataResponse.total,
|
||||
dataLength: dataResponse.data.length,
|
||||
columnsLength: columnsResponse.length,
|
||||
sampleData: dataResponse.data.slice(0, 2),
|
||||
sampleColumns: columnsResponse.slice(0, 3),
|
||||
});
|
||||
// console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, {
|
||||
// total: dataResponse.total,
|
||||
// dataLength: dataResponse.data.length,
|
||||
// columnsLength: columnsResponse.length,
|
||||
// sampleData: dataResponse.data.slice(0, 2),
|
||||
// sampleColumns: columnsResponse.slice(0, 3),
|
||||
// });
|
||||
|
||||
setLoadedTableData(dataResponse.data);
|
||||
setLoadedTableColumns(columnsResponse);
|
||||
@@ -130,32 +193,32 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
|
||||
// 표시할 데이터 결정 (로드된 테이블 데이터 우선 사용)
|
||||
const displayData = useMemo(() => {
|
||||
console.log("📋 CardDisplay: displayData 결정 중", {
|
||||
dataSource: componentConfig.dataSource,
|
||||
loadedTableDataLength: loadedTableData.length,
|
||||
tableDataLength: tableData.length,
|
||||
staticDataLength: componentConfig.staticData?.length || 0,
|
||||
});
|
||||
// console.log("📋 CardDisplay: displayData 결정 중", {
|
||||
// dataSource: componentConfig.dataSource,
|
||||
// loadedTableDataLength: loadedTableData.length,
|
||||
// tableDataLength: tableData.length,
|
||||
// staticDataLength: componentConfig.staticData?.length || 0,
|
||||
// });
|
||||
|
||||
// 로드된 테이블 데이터가 있으면 항상 우선 사용 (dataSource 설정 무시)
|
||||
if (loadedTableData.length > 0) {
|
||||
console.log("📋 CardDisplay: 로드된 테이블 데이터 사용", loadedTableData.slice(0, 2));
|
||||
// console.log("📋 CardDisplay: 로드된 테이블 데이터 사용", loadedTableData.slice(0, 2));
|
||||
return loadedTableData;
|
||||
}
|
||||
|
||||
// props로 전달받은 테이블 데이터가 있으면 사용
|
||||
if (tableData.length > 0) {
|
||||
console.log("📋 CardDisplay: props 테이블 데이터 사용", tableData.slice(0, 2));
|
||||
// console.log("📋 CardDisplay: props 테이블 데이터 사용", tableData.slice(0, 2));
|
||||
return tableData;
|
||||
}
|
||||
|
||||
if (componentConfig.staticData && componentConfig.staticData.length > 0) {
|
||||
console.log("📋 CardDisplay: 정적 데이터 사용", componentConfig.staticData.slice(0, 2));
|
||||
// console.log("📋 CardDisplay: 정적 데이터 사용", componentConfig.staticData.slice(0, 2));
|
||||
return componentConfig.staticData;
|
||||
}
|
||||
|
||||
// 데이터가 없으면 빈 배열 반환
|
||||
console.log("📋 CardDisplay: 표시할 데이터가 없음");
|
||||
// console.log("📋 CardDisplay: 표시할 데이터가 없음");
|
||||
return [];
|
||||
}, [componentConfig.dataSource, loadedTableData, tableData, componentConfig.staticData]);
|
||||
|
||||
@@ -260,23 +323,8 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// DOM에 전달하면 안 되는 React-specific props 필터링
|
||||
const {
|
||||
selectedScreen,
|
||||
onZoneComponentDrop,
|
||||
onZoneClick,
|
||||
componentConfig: _componentConfig,
|
||||
component: _component,
|
||||
isSelected: _isSelected,
|
||||
onClick: _onClick,
|
||||
onDragStart: _onDragStart,
|
||||
onDragEnd: _onDragEnd,
|
||||
size: _size,
|
||||
position: _position,
|
||||
style: _style,
|
||||
onRefresh: _onRefresh, // React DOM 속성이 아니므로 필터링
|
||||
...domProps
|
||||
} = props;
|
||||
// DOM 안전한 props만 필터링 (filterDOMProps 유틸리티 사용)
|
||||
const safeDomProps = filterDOMProps(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -301,7 +349,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
onClick={handleClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
{...domProps}
|
||||
{...safeDomProps}
|
||||
>
|
||||
<div style={containerStyle}>
|
||||
{displayData.length === 0 ? (
|
||||
@@ -393,8 +441,24 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
|
||||
{/* 카드 액션 (선택사항) */}
|
||||
<div className="mt-3 flex justify-end space-x-2">
|
||||
<button className="text-xs font-medium text-blue-600 hover:text-blue-800">상세보기</button>
|
||||
<button className="text-xs font-medium text-gray-500 hover:text-gray-700">편집</button>
|
||||
<button
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardView(data);
|
||||
}}
|
||||
>
|
||||
상세보기
|
||||
</button>
|
||||
<button
|
||||
className="text-xs font-medium text-gray-500 hover:text-gray-700 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCardEdit(data);
|
||||
}}
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -402,6 +466,101 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세보기 모달 */}
|
||||
<Dialog open={viewModalOpen} onOpenChange={setViewModalOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span className="text-lg">📋</span>
|
||||
상세 정보
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedData && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Object.entries(selectedData)
|
||||
.filter(([key, value]) => value !== null && value !== undefined && value !== '')
|
||||
.map(([key, value]) => (
|
||||
<div key={key} className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">
|
||||
{key.replace(/_/g, ' ')}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 break-words">
|
||||
{String(value)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<button
|
||||
onClick={() => setViewModalOpen(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 편집 모달 */}
|
||||
<Dialog open={editModalOpen} onOpenChange={setEditModalOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<span className="text-lg">✏️</span>
|
||||
데이터 편집
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editData && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{Object.entries(editData)
|
||||
.filter(([key, value]) => value !== null && value !== undefined)
|
||||
.map(([key, value]) => (
|
||||
<div key={key} className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 block">
|
||||
{key.replace(/_/g, ' ').toUpperCase()}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={String(value)}
|
||||
onChange={(e) => handleEditFormChange(key, e.target.value)}
|
||||
className="w-full"
|
||||
placeholder={`${key} 입력`}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditModalOpen(false);
|
||||
setEditData(null);
|
||||
}}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleEditSave}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user