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:
DDD1542
2026-02-12 11:42:52 +09:00
parent 0512a3214c
commit 4294e6206b
20 changed files with 555 additions and 481 deletions

View File

@@ -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];