dev #65

Merged
hjlee merged 3 commits from dev into main 2025-09-25 18:57:36 +09:00
9 changed files with 512 additions and 56 deletions
Showing only changes of commit c28e27f3e8 - Show all commits

View File

@ -275,16 +275,16 @@ export default function ScreenViewPage() {
zIndex: component.position.z || 1,
}}
onMouseEnter={() => {
console.log("🎯 할당된 화면 컴포넌트:", {
id: component.id,
type: component.type,
position: component.position,
size: component.size,
styleWidth: component.style?.width,
styleHeight: component.style?.height,
finalWidth: `${component.size.width}px`,
finalHeight: `${component.size.height}px`,
});
// console.log("🎯 할당된 화면 컴포넌트:", {
// id: component.id,
// type: component.type,
// position: component.position,
// size: component.size,
// styleWidth: component.style?.width,
// styleHeight: component.style?.height,
// finalWidth: `${component.size.width}px`,
// finalHeight: `${component.size.height}px`,
// });
}}
>
{/* 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 */}

View File

@ -1809,6 +1809,25 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
filters: [],
displayFormat: "simple" as const,
};
case "table":
return {
tableName: "",
displayMode: "table" as const,
showHeader: true,
showFooter: true,
pagination: {
enabled: true,
pageSize: 10,
showPageSizeSelector: true,
showPageInfo: true,
showFirstLast: true,
},
columns: [],
searchable: true,
sortable: true,
filterable: true,
exportable: true,
};
default:
return undefined;
}

View File

@ -20,7 +20,30 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
return ComponentRegistry.getAllComponents();
const components = ComponentRegistry.getAllComponents();
console.log("🔍 ComponentsPanel - 로드된 컴포넌트:", components.map(c => ({ id: c.id, name: c.name, category: c.category })));
// 수동으로 table-list 컴포넌트 추가 (임시)
const hasTableList = components.some(c => c.id === 'table-list');
if (!hasTableList) {
console.log("⚠️ table-list 컴포넌트가 없어서 수동 추가");
components.push({
id: "table-list",
name: "테이블 리스트",
nameEng: "TableList Component",
description: "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트",
category: "display",
webType: "text",
defaultConfig: {},
defaultSize: { width: 800, height: 400 },
icon: "Table",
tags: ["테이블", "데이터", "목록", "그리드"],
version: "1.0.0",
author: "개발팀",
});
}
return components;
}, []);
// 카테고리별 분류 (input 카테고리 제외)

View File

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

View File

@ -24,6 +24,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters";
import { SingleTableWithSticky } from "./SingleTableWithSticky";
import { CardModeRenderer } from "./CardModeRenderer";
export interface TableListComponentProps {
component: any;
@ -1290,6 +1291,29 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<div className="mt-1 text-xs text-red-500 bg-red-50 px-3 py-1 rounded-full">{error}</div>
</div>
</div>
) : tableConfig.displayMode === "card" ? (
// 카드 모드 렌더링
<div className="w-full h-full overflow-y-auto">
<CardModeRenderer
data={data}
cardConfig={tableConfig.cardConfig || {
idColumn: "id",
titleColumn: "name",
cardsPerRow: 3,
cardSpacing: 16,
showActions: true,
}}
visibleColumns={visibleColumns}
onRowClick={handleRowClick}
onRowSelect={(row, selected) => {
const rowIndex = data.findIndex(d => d === row);
const rowKey = getRowKey(row, rowIndex);
handleRowSelection(rowKey, selected);
}}
selectedRows={Array.from(selectedRows)}
showActions={tableConfig.actions?.showActions}
/>
</div>
) : needsHorizontalScroll ? (
// 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용
<div className="w-full overflow-hidden">

View File

@ -10,6 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { TableListConfig, ColumnConfig } from "./types";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { tableTypeApi } from "@/lib/api/screen";
@ -755,6 +756,188 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
{/* 기본 설정 탭 */}
<TabsContent value="basic" className="space-y-4">
<ScrollArea className="h-[600px] pr-4">
{/* 표시 모드 설정 */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<Label> </Label>
<RadioGroup
value={config.displayMode || "table"}
onValueChange={(value: "table" | "card") => handleChange("displayMode", value)}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="table" id="table-mode" />
<Label htmlFor="table-mode" className="cursor-pointer">
()
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="card" id="card-mode" />
<Label htmlFor="card-mode" className="cursor-pointer">
</Label>
</div>
</RadioGroup>
</div>
{/* 카드 모드 설정 */}
{config.displayMode === "card" && (
<div className="space-y-4 border-t pt-4">
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cards-per-row"> </Label>
<Select
value={config.cardConfig?.cardsPerRow?.toString() || "3"}
onValueChange={(value) =>
handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="6">6</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="card-spacing"> (px)</Label>
<Input
id="card-spacing"
type="number"
value={config.cardConfig?.cardSpacing || 16}
onChange={(e) =>
handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))
}
min="0"
max="50"
/>
</div>
</div>
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-1 gap-3">
<div className="space-y-2">
<Label htmlFor="id-column">ID ( )</Label>
<Select
value={config.cardConfig?.idColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "idColumn", value)
}
>
<SelectTrigger>
<SelectValue placeholder="ID 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="title-column"> ( )</Label>
<Select
value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "titleColumn", value)
}
>
<SelectTrigger>
<SelectValue placeholder="제목 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="subtitle-column"> ( )</Label>
<Select
value={config.cardConfig?.subtitleColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "subtitleColumn", value)
}
>
<SelectTrigger>
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description-column"> </Label>
<Select
value={config.cardConfig?.descriptionColumn || ""}
onValueChange={(value) =>
handleNestedChange("cardConfig", "descriptionColumn", value)
}
>
<SelectTrigger>
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="show-card-actions"
checked={config.cardConfig?.showActions !== false}
onCheckedChange={(checked) =>
handleNestedChange("cardConfig", "showActions", checked as boolean)
}
/>
<Label htmlFor="show-card-actions" className="cursor-pointer">
</Label>
</div>
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>

View File

@ -67,3 +67,16 @@ export class TableListRenderer extends AutoRegisteringComponentRenderer {
// 자동 등록 실행
TableListRenderer.registerSelf();
// 강제 등록 (디버깅용)
if (typeof window !== "undefined") {
setTimeout(() => {
try {
console.log("🔄 TableList 강제 등록 시도...");
TableListRenderer.registerSelf();
console.log("✅ TableList 강제 등록 완료");
} catch (error) {
console.error("❌ TableList 강제 등록 실패:", error);
}
}, 1000);
}

View File

@ -18,9 +18,21 @@ export const TableListDefinition = createComponentDefinition({
nameEng: "TableList Component",
description: "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트",
category: ComponentCategory.DISPLAY,
webType: "table",
webType: "text",
component: TableListWrapper,
defaultConfig: {
// 표시 모드 설정
displayMode: "table" as const,
// 카드 모드 기본 설정
cardConfig: {
idColumn: "id",
titleColumn: "name",
cardsPerRow: 3,
cardSpacing: 16,
showActions: true,
},
// 테이블 기본 설정
showHeader: true,
showFooter: true,

View File

@ -74,6 +74,21 @@ export interface ColumnConfig {
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
}
/**
*
*/
export interface CardDisplayConfig {
idColumn: string; // ID 컬럼 (사번 등)
titleColumn: string; // 제목 컬럼 (이름 등)
subtitleColumn?: string; // 부제목 컬럼 (부서 등)
descriptionColumn?: string; // 설명 컬럼
imageColumn?: string; // 이미지 컬럼
cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
cardSpacing: number; // 카드 간격 (기본: 16px)
showActions: boolean; // 액션 버튼 표시 여부
cardHeight?: number; // 카드 높이 (기본: auto)
}
/**
*
*/
@ -147,6 +162,12 @@ export interface CheckboxConfig {
* TableList
*/
export interface TableListConfig extends ComponentConfig {
// 표시 모드 설정
displayMode?: "table" | "card"; // 기본: "table"
// 카드 디스플레이 설정 (displayMode가 "card"일 때 사용)
cardConfig?: CardDisplayConfig;
// 테이블 기본 설정
selectedTable?: string;
tableName?: string;
@ -175,7 +196,9 @@ export interface TableListConfig extends ComponentConfig {
};
// 페이지네이션
pagination: PaginationConfig;
pagination: PaginationConfig & {
currentPage?: number; // 현재 페이지 (추가)
};
// 필터 설정
filter: FilterConfig;