feat: Add express-async-errors for improved error handling
- Integrated express-async-errors to automatically handle errors in async route handlers, enhancing the overall error management in the application. - Updated app.ts to include the express-async-errors import for global error handling. - Removed redundant logging statements in admin and user menu retrieval functions to streamline the code and improve readability. - Adjusted logging levels from info to debug for less critical logs, ensuring that important information is logged appropriately without cluttering the logs.
This commit is contained in:
@@ -3486,7 +3486,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
<CardContent className="flex-1 overflow-hidden p-4">
|
||||
{/* 추가 탭 컨텐츠 */}
|
||||
{activeTabIndex > 0 ? (
|
||||
(() => {
|
||||
@@ -3513,103 +3513,226 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
// 탭 컬럼 설정
|
||||
const tabColumns = currentTabConfig?.columns || [];
|
||||
|
||||
// 테이블 모드로 표시
|
||||
// 테이블 모드로 표시 (행 클릭 시 상세 정보 펼치기)
|
||||
if (currentTabConfig?.displayMode === "table") {
|
||||
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
|
||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||
const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
{tabColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-medium">
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{tabSummaryColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
))}
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-medium">작업</th>
|
||||
{hasTabActions && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentTabData.map((item: any, idx: number) => (
|
||||
<tr key={item.id || idx} className="hover:bg-muted/50 border-b">
|
||||
{tabColumns.map((col: any) => (
|
||||
<td key={col.name} className="px-3 py-2 text-xs">
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
{currentTabData.map((item: any, idx: number) => {
|
||||
const tabItemId = item.id || item.ID || idx;
|
||||
const isTabExpanded = expandedRightItems.has(`tab_${activeTabIndex}_${tabItemId}`);
|
||||
|
||||
// 상세 정보용 전체 값 목록 (showInDetail이 false가 아닌 것만)
|
||||
const tabDetailColumns = tabColumns.filter((col: any) => col.showInDetail !== false);
|
||||
const tabAllValues: [string, any, string][] = tabDetailColumns.length > 0
|
||||
? tabDetailColumns.map((col: any) => [col.name, getEntityJoinValue(item, col.name), col.label || col.name] as [string, any, string])
|
||||
: Object.entries(item)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== "")
|
||||
.map(([k, v]) => [k, v, ""] as [string, any, string]);
|
||||
|
||||
return (
|
||||
<React.Fragment key={tabItemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={() => handleEditClick("right", item)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={() => handleDeleteClick("right", item, currentTabConfig?.tableName)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||
>
|
||||
{tabSummaryColumns.map((col: any) => (
|
||||
<td key={col.name} className="px-3 py-2 text-xs">
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{hasTabActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("right", item);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("right", item, currentTabConfig?.tableName);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
{/* 상세 정보 (행 클릭 시 펼쳐짐) */}
|
||||
{isTabExpanded && (
|
||||
<tr>
|
||||
<td colSpan={tabSummaryColumns.length + (hasTabActions ? 1 : 0)} className="bg-muted/30 px-3 py-2">
|
||||
<div className="mb-1 text-xs font-semibold">상세 정보</div>
|
||||
<div className="bg-card overflow-auto rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody className="divide-border divide-y">
|
||||
{tabAllValues.map(([key, value, label]) => {
|
||||
const displayValue = (value === null || value === undefined || value === "")
|
||||
? "-"
|
||||
: formatCellValue(key, value, rightCategoryMappings);
|
||||
return (
|
||||
<tr key={key} className="hover:bg-muted">
|
||||
<td className="text-muted-foreground px-3 py-1.5 text-xs font-medium whitespace-nowrap">
|
||||
{label || getColumnLabel(key)}
|
||||
</td>
|
||||
<td className="text-foreground px-3 py-1.5 text-xs break-all">{displayValue}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 리스트(카드) 모드로 표시
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{currentTabData.map((item: any, idx: number) => (
|
||||
<div key={item.id || idx} className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{tabColumns.map((col: any) => (
|
||||
<span key={col.name}>
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<div className="flex items-center gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={() => handleEditClick("right", item)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
// 리스트 모드도 테이블형으로 통일 (행 클릭 시 상세 정보 표시)
|
||||
{
|
||||
const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete;
|
||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||
const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false);
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{listSummaryColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
))}
|
||||
{hasTabActions && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={() => handleDeleteClick("right", item, currentTabConfig?.tableName)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentTabData.map((item: any, idx: number) => {
|
||||
const tabItemId = item.id || item.ID || idx;
|
||||
const isTabExpanded = expandedRightItems.has(`tab_${activeTabIndex}_${tabItemId}`);
|
||||
// showInDetail이 false가 아닌 것만 상세에 표시
|
||||
const listDetailColumns = tabColumns.filter((col: any) => col.showInDetail !== false);
|
||||
const tabAllValues: [string, any, string][] = listDetailColumns.length > 0
|
||||
? listDetailColumns.map((col: any) => [col.name, getEntityJoinValue(item, col.name), col.label || col.name] as [string, any, string])
|
||||
: Object.entries(item)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== "")
|
||||
.map(([k, v]) => [k, v, ""] as [string, any, string]);
|
||||
|
||||
return (
|
||||
<React.Fragment key={tabItemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
isTabExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)}
|
||||
>
|
||||
{listSummaryColumns.map((col: any) => (
|
||||
<td key={col.name} className="px-3 py-2 text-xs">
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{hasTabActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={(e) => { e.stopPropagation(); handleEditClick("right", item); }}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteClick("right", item, currentTabConfig?.tableName); }}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
{isTabExpanded && (
|
||||
<tr>
|
||||
<td colSpan={listSummaryColumns.length + (hasTabActions ? 1 : 0)} className="bg-muted/30 px-3 py-2">
|
||||
<div className="mb-1 text-xs font-semibold">상세 정보</div>
|
||||
<div className="bg-card overflow-auto rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody className="divide-border divide-y">
|
||||
{tabAllValues.map(([key, value, label]) => {
|
||||
const displayValue = (value === null || value === undefined || value === "")
|
||||
? "-" : formatCellValue(key, value, rightCategoryMappings);
|
||||
return (
|
||||
<tr key={key} className="hover:bg-muted">
|
||||
<td className="text-muted-foreground px-3 py-1.5 text-xs font-medium whitespace-nowrap">
|
||||
{label || getColumnLabel(key)}
|
||||
</td>
|
||||
<td className="text-foreground px-3 py-1.5 text-xs break-all">{displayValue}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()
|
||||
) : componentConfig.rightPanel?.displayMode === "custom" ? (
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||
@@ -3860,12 +3983,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
let columnsToShow: any[] = [];
|
||||
|
||||
if (displayColumns.length > 0) {
|
||||
// 설정된 컬럼 사용
|
||||
columnsToShow = displayColumns.map((col) => ({
|
||||
...col,
|
||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||
format: col.format,
|
||||
}));
|
||||
// 설정된 컬럼 사용 (showInSummary가 false가 아닌 것만 테이블에 표시)
|
||||
columnsToShow = displayColumns
|
||||
.filter((col) => col.showInSummary !== false)
|
||||
.map((col) => ({
|
||||
...col,
|
||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||
format: col.format,
|
||||
}));
|
||||
|
||||
// 🆕 그룹 합산 모드이고, 키 컬럼이 표시 목록에 없으면 맨 앞에 추가
|
||||
if (isGroupedMode && keyColumns.length > 0) {
|
||||
@@ -3900,21 +4025,15 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="text-muted-foreground mb-2 text-xs">
|
||||
{filteredData.length}개의 관련 데이터
|
||||
{rightSearchQuery && filteredData.length !== rightData.length && (
|
||||
<span className="text-primary ml-1">(전체 {rightData.length}개 중)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 z-10 bg-gray-50">
|
||||
<tr>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="min-w-full">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap"
|
||||
className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold whitespace-nowrap"
|
||||
style={{
|
||||
width: col.width ? `${col.width}px` : "auto",
|
||||
minWidth: "80px",
|
||||
@@ -3928,22 +4047,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
{!isDesignMode &&
|
||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">
|
||||
작업
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
<tbody>
|
||||
{filteredData.map((item, idx) => {
|
||||
const itemId = item.id || item.ID || idx;
|
||||
|
||||
return (
|
||||
<tr key={itemId} className="hover:bg-accent transition-colors">
|
||||
<tr key={itemId} className={cn("border-b border-border/40 transition-colors hover:bg-muted/30", idx % 2 === 1 && "bg-muted/10")}>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
key={colIdx}
|
||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||
className="px-3 py-2 text-xs whitespace-nowrap"
|
||||
style={{ textAlign: col.align || "left" }}
|
||||
>
|
||||
{formatCellValue(
|
||||
@@ -4001,176 +4120,155 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
);
|
||||
}
|
||||
|
||||
// 목록 모드 (기존)
|
||||
return filteredData.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-muted-foreground mb-2 text-xs">
|
||||
{filteredData.length}개의 관련 데이터
|
||||
{rightSearchQuery && filteredData.length !== rightData.length && (
|
||||
<span className="text-primary ml-1">(전체 {rightData.length}개 중)</span>
|
||||
)}
|
||||
</div>
|
||||
{filteredData.map((item, index) => {
|
||||
const itemId = item.id || item.ID || index;
|
||||
const isExpanded = expandedRightItems.has(itemId);
|
||||
// 목록 모드 - 테이블형 디자인 (행 클릭 시 상세 정보 표시)
|
||||
{
|
||||
// 표시 컬럼 결정
|
||||
const rightColumns = componentConfig.rightPanel?.columns;
|
||||
let columnsToDisplay: { name: string; label: string; format?: string; bold?: boolean }[] = [];
|
||||
|
||||
// 우측 패널 표시 컬럼 설정 확인
|
||||
const rightColumns = componentConfig.rightPanel?.columns;
|
||||
let firstValues: [string, any, string][] = [];
|
||||
let allValues: [string, any, string][] = [];
|
||||
if (rightColumns && rightColumns.length > 0) {
|
||||
// showInSummary가 false가 아닌 것만 메인 테이블에 표시
|
||||
columnsToDisplay = rightColumns
|
||||
.filter((col) => col.showInSummary !== false)
|
||||
.map((col) => ({
|
||||
name: col.name,
|
||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||
format: col.format,
|
||||
bold: col.bold,
|
||||
}));
|
||||
} else if (filteredData.length > 0) {
|
||||
columnsToDisplay = Object.keys(filteredData[0])
|
||||
.filter((key) => shouldShowField(key))
|
||||
.slice(0, 6)
|
||||
.map((key) => ({
|
||||
name: key,
|
||||
label: rightColumnLabels[key] || key,
|
||||
}));
|
||||
}
|
||||
|
||||
if (rightColumns && rightColumns.length > 0) {
|
||||
// 설정된 컬럼만 표시 (엔티티 조인 컬럼 처리)
|
||||
// 설정된 컬럼은 null/empty여도 항상 표시 (사용자가 명시적으로 설정한 컬럼이므로)
|
||||
const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3;
|
||||
firstValues = rightColumns
|
||||
.slice(0, summaryCount)
|
||||
.map((col) => {
|
||||
const value = getEntityJoinValue(item, col.name);
|
||||
return [col.name, value, col.label] as [string, any, string];
|
||||
});
|
||||
const hasEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true);
|
||||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
||||
const hasActions = hasEditButton || hasDeleteButton;
|
||||
|
||||
allValues = rightColumns
|
||||
.map((col) => {
|
||||
const value = getEntityJoinValue(item, col.name);
|
||||
return [col.name, value, col.label] as [string, any, string];
|
||||
});
|
||||
} else {
|
||||
// 설정 없으면 모든 컬럼 표시 (기존 로직)
|
||||
const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3;
|
||||
firstValues = Object.entries(item)
|
||||
.filter(([key]) => !key.toLowerCase().includes("id"))
|
||||
.slice(0, summaryCount)
|
||||
.map(([key, value]) => [key, value, ""] as [string, any, string]);
|
||||
return filteredData.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{columnsToDisplay.map((col) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-semibold">
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{hasActions && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-semibold">작업</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData.map((item, idx) => {
|
||||
const itemId = item.id || item.ID || idx;
|
||||
const isExpanded = expandedRightItems.has(itemId);
|
||||
|
||||
allValues = Object.entries(item)
|
||||
.filter(([key, value]) => value !== null && value !== undefined && value !== "")
|
||||
.map(([key, value]) => [key, value, ""] as [string, any, string]);
|
||||
}
|
||||
// 상세 정보용 전체 값 목록 (showInDetail이 false가 아닌 것만 표시)
|
||||
let allValues: [string, any, string][] = [];
|
||||
if (rightColumns && rightColumns.length > 0) {
|
||||
allValues = rightColumns
|
||||
.filter((col) => col.showInDetail !== false)
|
||||
.map((col) => {
|
||||
const value = getEntityJoinValue(item, col.name);
|
||||
return [col.name, value, col.label] as [string, any, string];
|
||||
});
|
||||
} else {
|
||||
allValues = Object.entries(item)
|
||||
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
||||
.map(([key, value]) => [key, value, ""] as [string, any, string]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={itemId}
|
||||
className="bg-card overflow-hidden rounded-lg border shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
{/* 요약 정보 */}
|
||||
<div className="p-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div
|
||||
className="min-w-0 flex-1 cursor-pointer"
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
{firstValues.map(([key, value, label], idx) => {
|
||||
// 포맷 설정 및 볼드 설정 찾기
|
||||
const colConfig = rightColumns?.find((c) => c.name === key);
|
||||
const format = colConfig?.format;
|
||||
const boldValue = colConfig?.bold ?? false;
|
||||
|
||||
// 🆕 포맷 적용 (날짜/숫자/카테고리) - null/empty는 "-"로 표시
|
||||
const displayValue = (value === null || value === undefined || value === "")
|
||||
? "-"
|
||||
: formatCellValue(key, value, rightCategoryMappings, format);
|
||||
|
||||
const showLabel = componentConfig.rightPanel?.summaryShowLabel ?? true;
|
||||
|
||||
return (
|
||||
<div key={key} className="flex items-baseline gap-1">
|
||||
{showLabel && (
|
||||
<span className="text-muted-foreground text-xs font-medium whitespace-nowrap">
|
||||
{label || getColumnLabel(key)}:
|
||||
</span>
|
||||
return (
|
||||
<React.Fragment key={itemId}>
|
||||
<tr
|
||||
className={cn(
|
||||
"cursor-pointer border-b border-border/40 transition-colors",
|
||||
isExpanded ? "bg-primary/5" : idx % 2 === 1 ? "bg-muted/10 hover:bg-muted/30" : "hover:bg-muted/30",
|
||||
)}
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
>
|
||||
{columnsToDisplay.map((col) => (
|
||||
<td key={col.name} className="px-3 py-2 text-xs">
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
<span
|
||||
className={`text-foreground text-sm ${boldValue ? "font-semibold" : ""}`}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-start gap-1 pt-1">
|
||||
{/* 수정 버튼 */}
|
||||
{!isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
||||
<Button
|
||||
variant={componentConfig.rightPanel?.editButton?.buttonVariant || "outline"}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("right", item);
|
||||
}}
|
||||
className="h-7"
|
||||
>
|
||||
<Pencil className="mr-1 h-3 w-3" />
|
||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||
</Button>
|
||||
)}
|
||||
{/* 삭제 버튼 */}
|
||||
{!isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("right", item);
|
||||
}}
|
||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</button>
|
||||
)}
|
||||
{/* 확장/접기 버튼 */}
|
||||
<button
|
||||
onClick={() => toggleRightItemExpansion(itemId)}
|
||||
className="rounded p-1 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="text-muted-foreground h-5 w-5" />
|
||||
) : (
|
||||
<ChevronDown className="text-muted-foreground h-5 w-5" />
|
||||
</td>
|
||||
))}
|
||||
{hasActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{hasEditButton && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("right", item);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{hasDeleteButton && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("right", item);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
{/* 상세 정보 (행 클릭 시 펼쳐짐) */}
|
||||
{isExpanded && (
|
||||
<tr>
|
||||
<td colSpan={columnsToDisplay.length + (hasActions ? 1 : 0)} className="bg-muted/30 px-3 py-2">
|
||||
<div className="mb-1 text-xs font-semibold">상세 정보</div>
|
||||
<div className="bg-card overflow-auto rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody className="divide-border divide-y">
|
||||
{allValues.map(([key, value, label]) => {
|
||||
const colConfig = rightColumns?.find((c) => c.name === key);
|
||||
const format = colConfig?.format;
|
||||
const displayValue = (value === null || value === undefined || value === "")
|
||||
? "-"
|
||||
: formatCellValue(key, value, rightCategoryMappings, format);
|
||||
return (
|
||||
<tr key={key} className="hover:bg-muted">
|
||||
<td className="text-muted-foreground px-3 py-1.5 text-xs font-medium whitespace-nowrap">
|
||||
{label || getColumnLabel(key)}
|
||||
</td>
|
||||
<td className="text-foreground px-3 py-1.5 text-xs break-all">{displayValue}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세 정보 (확장 시 표시) */}
|
||||
{isExpanded && (
|
||||
<div className="bg-muted/50 border-t px-3 py-2">
|
||||
<div className="mb-2 text-xs font-semibold">전체 상세 정보</div>
|
||||
<div className="bg-card overflow-auto rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody className="divide-border divide-y">
|
||||
{allValues.map(([key, value, label]) => {
|
||||
// 포맷 설정 찾기
|
||||
const colConfig = rightColumns?.find((c) => c.name === key);
|
||||
const format = colConfig?.format;
|
||||
|
||||
// 🆕 포맷 적용 (날짜/숫자/카테고리) - null/empty는 "-"로 표시
|
||||
const displayValue = (value === null || value === undefined || value === "")
|
||||
? "-"
|
||||
: formatCellValue(key, value, rightCategoryMappings, format);
|
||||
|
||||
return (
|
||||
<tr key={key} className="hover:bg-muted">
|
||||
<td className="text-muted-foreground px-3 py-2 font-medium whitespace-nowrap">
|
||||
{label || getColumnLabel(key)}
|
||||
</td>
|
||||
<td className="text-foreground px-3 py-2 break-all">{displayValue}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
{rightSearchQuery ? (
|
||||
<>
|
||||
@@ -4182,6 +4280,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()
|
||||
) : (
|
||||
// 상세 모드: 단일 객체를 상세 정보로 표시
|
||||
@@ -4198,8 +4297,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
rightColumns.map((c) => `${c.name} (${c.label})`),
|
||||
);
|
||||
|
||||
// 설정된 컬럼만 표시
|
||||
// 설정된 컬럼만 표시 (showInDetail이 false가 아닌 것만)
|
||||
displayEntries = rightColumns
|
||||
.filter((col) => col.showInDetail !== false)
|
||||
.map((col) => {
|
||||
// 🆕 엔티티 조인 컬럼 처리 (예: item_info.item_name → item_name)
|
||||
let value = rightData[col.name];
|
||||
|
||||
Reference in New Issue
Block a user