feat: Enhance V2Repeater and configuration panel with source detail auto-fetching
- Added support for automatic fetching of detail rows from the master data in the V2Repeater component, improving data management. - Introduced a new configuration option in the V2RepeaterConfigPanel to enable source detail auto-fetching, allowing users to specify detail table and foreign key settings. - Enhanced the V2Repeater component to handle entity joins for loading data, optimizing data retrieval processes. - Updated the V2RepeaterProps and V2RepeaterConfig interfaces to include new properties for grouped data and source detail configuration, ensuring type safety and clarity in component usage. - Improved logging for data loading processes to provide better insights during development and debugging.
This commit is contained in:
@@ -36,6 +36,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { toast } from "sonner";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
|
||||
|
||||
export interface SplitPanelLayout2ComponentProps extends ComponentRendererProps {
|
||||
// 추가 props
|
||||
@@ -92,6 +93,9 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
const [leftActiveTab, setLeftActiveTab] = useState<string | null>(null);
|
||||
const [rightActiveTab, setRightActiveTab] = useState<string | null>(null);
|
||||
|
||||
// 카테고리 코드→라벨 매핑
|
||||
const [categoryLabelMap, setCategoryLabelMap] = useState<Record<string, string>>({});
|
||||
|
||||
// 프론트엔드 그룹핑 함수
|
||||
const groupData = useCallback(
|
||||
(data: Record<string, any>[], groupingConfig: GroupingConfig, columns: ColumnConfig[]): Record<string, any>[] => {
|
||||
@@ -185,17 +189,17 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
}
|
||||
});
|
||||
|
||||
// 탭 목록 생성
|
||||
// 탭 목록 생성 (카테고리 라벨 변환 적용)
|
||||
const tabs = Array.from(valueCount.entries()).map(([value, count]) => ({
|
||||
id: value,
|
||||
label: value,
|
||||
label: categoryLabelMap[value] || value,
|
||||
count: tabConfig.showCount ? count : 0,
|
||||
}));
|
||||
|
||||
console.log(`[SplitPanelLayout2] 탭 생성: ${tabs.length}개 탭 (컬럼: ${sourceColumn})`);
|
||||
return tabs;
|
||||
},
|
||||
[],
|
||||
[categoryLabelMap],
|
||||
);
|
||||
|
||||
// 탭으로 필터링된 데이터 반환
|
||||
@@ -1000,10 +1004,38 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
console.log("[SplitPanelLayout2] 좌측 액션 버튼 모달 열기:", modalScreenId);
|
||||
break;
|
||||
|
||||
case "edit":
|
||||
// 좌측 패널에서 수정 (필요시 구현)
|
||||
console.log("[SplitPanelLayout2] 좌측 수정 액션:", btn);
|
||||
case "edit": {
|
||||
if (!selectedLeftItem) {
|
||||
toast.error("수정할 항목을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const editModalScreenId = btn.modalScreenId || config.leftPanel?.editModalScreenId || config.leftPanel?.addModalScreenId;
|
||||
|
||||
if (!editModalScreenId) {
|
||||
toast.error("연결된 모달 화면이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const editEvent = new CustomEvent("openEditModal", {
|
||||
detail: {
|
||||
screenId: editModalScreenId,
|
||||
title: btn.label || "수정",
|
||||
modalSize: "lg",
|
||||
editData: selectedLeftItem,
|
||||
isCreateMode: false,
|
||||
onSave: () => {
|
||||
loadLeftData();
|
||||
if (selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(editEvent);
|
||||
console.log("[SplitPanelLayout2] 좌측 수정 모달 열기:", selectedLeftItem);
|
||||
break;
|
||||
}
|
||||
|
||||
case "delete":
|
||||
// 좌측 패널에서 삭제 (필요시 구현)
|
||||
@@ -1018,7 +1050,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
break;
|
||||
}
|
||||
},
|
||||
[config.leftPanel?.addModalScreenId, loadLeftData],
|
||||
[config.leftPanel?.addModalScreenId, config.leftPanel?.editModalScreenId, loadLeftData, loadRightData, selectedLeftItem],
|
||||
);
|
||||
|
||||
// 컬럼 라벨 로드
|
||||
@@ -1241,6 +1273,55 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
config.rightPanel?.tableName,
|
||||
]);
|
||||
|
||||
// 카테고리 컬럼에 대한 라벨 매핑 로드
|
||||
useEffect(() => {
|
||||
if (isDesignMode) return;
|
||||
|
||||
const loadCategoryLabels = async () => {
|
||||
const allColumns = new Set<string>();
|
||||
const tableName = config.leftPanel?.tableName || config.rightPanel?.tableName;
|
||||
if (!tableName) return;
|
||||
|
||||
// 좌우 패널의 표시 컬럼에서 카테고리 후보 수집
|
||||
for (const col of config.leftPanel?.displayColumns || []) {
|
||||
allColumns.add(col.name);
|
||||
}
|
||||
for (const col of config.rightPanel?.displayColumns || []) {
|
||||
allColumns.add(col.name);
|
||||
}
|
||||
// 탭 소스 컬럼도 추가
|
||||
if (config.rightPanel?.tabConfig?.tabSourceColumn) {
|
||||
allColumns.add(config.rightPanel.tabConfig.tabSourceColumn);
|
||||
}
|
||||
if (config.leftPanel?.tabConfig?.tabSourceColumn) {
|
||||
allColumns.add(config.leftPanel.tabConfig.tabSourceColumn);
|
||||
}
|
||||
|
||||
const labelMap: Record<string, string> = {};
|
||||
|
||||
for (const columnName of allColumns) {
|
||||
try {
|
||||
const result = await getCategoryValues(tableName, columnName);
|
||||
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
|
||||
for (const item of result.data) {
|
||||
if (item.valueCode && item.valueLabel) {
|
||||
labelMap[item.valueCode] = item.valueLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 카테고리가 아닌 컬럼은 무시
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(labelMap).length > 0) {
|
||||
setCategoryLabelMap(labelMap);
|
||||
}
|
||||
};
|
||||
|
||||
loadCategoryLabels();
|
||||
}, [isDesignMode, config.leftPanel?.tableName, config.rightPanel?.tableName, config.leftPanel?.displayColumns, config.rightPanel?.displayColumns, config.rightPanel?.tabConfig?.tabSourceColumn, config.leftPanel?.tabConfig?.tabSourceColumn]);
|
||||
|
||||
// 컴포넌트 언마운트 시 DataProvider 해제
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -1250,6 +1331,23 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
};
|
||||
}, [screenContext, component.id]);
|
||||
|
||||
// 카테고리 코드를 라벨로 변환
|
||||
const resolveCategoryLabel = useCallback(
|
||||
(value: any): string => {
|
||||
if (value === null || value === undefined) return "";
|
||||
const strVal = String(value);
|
||||
if (categoryLabelMap[strVal]) return categoryLabelMap[strVal];
|
||||
// 콤마 구분 다중 값 처리
|
||||
if (strVal.includes(",")) {
|
||||
const codes = strVal.split(",").map((c) => c.trim()).filter(Boolean);
|
||||
const labels = codes.map((code) => categoryLabelMap[code] || code);
|
||||
return labels.join(", ");
|
||||
}
|
||||
return strVal;
|
||||
},
|
||||
[categoryLabelMap],
|
||||
);
|
||||
|
||||
// 컬럼 값 가져오기 (sourceTable 및 엔티티 참조 고려)
|
||||
const getColumnValue = useCallback(
|
||||
(item: any, col: ColumnConfig): any => {
|
||||
@@ -1547,7 +1645,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
const displayColumns = config.leftPanel?.displayColumns || [];
|
||||
const pkColumn = getLeftPrimaryKeyColumn();
|
||||
|
||||
// 값 렌더링 (배지 지원)
|
||||
// 값 렌더링 (배지 지원 + 카테고리 라벨 변환)
|
||||
const renderCellValue = (item: any, col: ColumnConfig) => {
|
||||
const value = item[col.name];
|
||||
if (value === null || value === undefined) return "-";
|
||||
@@ -1558,7 +1656,7 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((v, vIdx) => (
|
||||
<Badge key={vIdx} variant="secondary" className="text-xs">
|
||||
{formatValue(v, col.format)}
|
||||
{resolveCategoryLabel(v) || formatValue(v, col.format)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -1567,14 +1665,17 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
|
||||
// 배지 타입이지만 단일 값인 경우
|
||||
if (col.displayConfig?.displayType === "badge") {
|
||||
const label = resolveCategoryLabel(value);
|
||||
return (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatValue(value, col.format)}
|
||||
{label !== String(value) ? label : formatValue(value, col.format)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// 기본 텍스트
|
||||
// 카테고리 라벨 변환 시도 후 기본 텍스트
|
||||
const label = resolveCategoryLabel(value);
|
||||
if (label !== String(value)) return label;
|
||||
return formatValue(value, col.format);
|
||||
};
|
||||
|
||||
@@ -1821,9 +1922,12 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{displayColumns.map((col, colIdx) => (
|
||||
<TableCell key={colIdx}>{formatValue(getColumnValue(item, col), col.format)}</TableCell>
|
||||
))}
|
||||
{displayColumns.map((col, colIdx) => {
|
||||
const rawVal = getColumnValue(item, col);
|
||||
const resolved = resolveCategoryLabel(rawVal);
|
||||
const display = resolved !== String(rawVal ?? "") ? resolved : formatValue(rawVal, col.format);
|
||||
return <TableCell key={colIdx}>{display || "-"}</TableCell>;
|
||||
})}
|
||||
{(config.rightPanel?.showEditButton || config.rightPanel?.showDeleteButton) && (
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-1">
|
||||
@@ -2133,7 +2237,12 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
|
||||
// 새로운 actionButtons 배열 기반 렌더링 (빈 배열이면 버튼 없음)
|
||||
config.leftPanel.actionButtons.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
{config.leftPanel.actionButtons.map((btn, idx) => (
|
||||
{config.leftPanel.actionButtons
|
||||
.filter((btn) => {
|
||||
if (btn.showCondition === "selected") return !!selectedLeftItem;
|
||||
return true;
|
||||
})
|
||||
.map((btn, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user