feat: Enhance V2RepeaterConfigPanel with entity join column management
- Updated the toggleEntityJoinColumn function to include an optional columnType parameter for better flexibility in handling join columns. - Improved the logic for managing entity joins and columns, ensuring that columns are correctly added or removed based on user interactions. - Introduced a new section in the UI to display entity join columns in a read-only format, providing users with clear visibility of the join configurations. - Added loading states and messages to enhance user experience during data retrieval for entity joins. These changes aim to improve the functionality and usability of the V2RepeaterConfigPanel in managing entity relationships.
This commit is contained in:
parent
09d16e6672
commit
238a7d1db4
|
|
@ -375,12 +375,15 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
// Entity 조인 컬럼 토글 (추가/제거)
|
||||
const toggleEntityJoinColumn = useCallback(
|
||||
(joinTableName: string, sourceColumn: string, refColumnName: string, refColumnLabel: string, displayField: string) => {
|
||||
(joinTableName: string, sourceColumn: string, refColumnName: string, refColumnLabel: string, displayField: string, columnType?: string) => {
|
||||
const currentJoins = config.entityJoins || [];
|
||||
const existingJoinIdx = currentJoins.findIndex(
|
||||
(j) => j.sourceColumn === sourceColumn && j.referenceTable === joinTableName,
|
||||
);
|
||||
|
||||
let newEntityJoins = [...currentJoins];
|
||||
let newColumns = [...config.columns];
|
||||
|
||||
if (existingJoinIdx >= 0) {
|
||||
const existingJoin = currentJoins[existingJoinIdx];
|
||||
const existingColIdx = existingJoin.columns.findIndex((c) => c.referenceField === refColumnName);
|
||||
|
|
@ -388,34 +391,49 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
if (existingColIdx >= 0) {
|
||||
const updatedColumns = existingJoin.columns.filter((_, i) => i !== existingColIdx);
|
||||
if (updatedColumns.length === 0) {
|
||||
updateConfig({ entityJoins: currentJoins.filter((_, i) => i !== existingJoinIdx) });
|
||||
newEntityJoins = newEntityJoins.filter((_, i) => i !== existingJoinIdx);
|
||||
} else {
|
||||
const updated = [...currentJoins];
|
||||
updated[existingJoinIdx] = { ...existingJoin, columns: updatedColumns };
|
||||
updateConfig({ entityJoins: updated });
|
||||
newEntityJoins[existingJoinIdx] = { ...existingJoin, columns: updatedColumns };
|
||||
}
|
||||
// config.columns에서도 제거
|
||||
newColumns = newColumns.filter(c => !(c.key === displayField && c.isJoinColumn));
|
||||
} else {
|
||||
const updated = [...currentJoins];
|
||||
updated[existingJoinIdx] = {
|
||||
newEntityJoins[existingJoinIdx] = {
|
||||
...existingJoin,
|
||||
columns: [...existingJoin.columns, { referenceField: refColumnName, displayField }],
|
||||
};
|
||||
updateConfig({ entityJoins: updated });
|
||||
// config.columns에 추가
|
||||
newColumns.push({
|
||||
key: displayField,
|
||||
title: refColumnLabel,
|
||||
width: "auto",
|
||||
visible: true,
|
||||
editable: false,
|
||||
isJoinColumn: true,
|
||||
inputType: columnType || "text",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
updateConfig({
|
||||
entityJoins: [
|
||||
...currentJoins,
|
||||
{
|
||||
sourceColumn,
|
||||
referenceTable: joinTableName,
|
||||
columns: [{ referenceField: refColumnName, displayField }],
|
||||
},
|
||||
],
|
||||
newEntityJoins.push({
|
||||
sourceColumn,
|
||||
referenceTable: joinTableName,
|
||||
columns: [{ referenceField: refColumnName, displayField }],
|
||||
});
|
||||
// config.columns에 추가
|
||||
newColumns.push({
|
||||
key: displayField,
|
||||
title: refColumnLabel,
|
||||
width: "auto",
|
||||
visible: true,
|
||||
editable: false,
|
||||
isJoinColumn: true,
|
||||
inputType: columnType || "text",
|
||||
});
|
||||
}
|
||||
|
||||
updateConfig({ entityJoins: newEntityJoins, columns: newColumns });
|
||||
},
|
||||
[config.entityJoins, updateConfig],
|
||||
[config.entityJoins, config.columns, updateConfig],
|
||||
);
|
||||
|
||||
// Entity 조인에 특정 컬럼이 설정되어 있는지 확인
|
||||
|
|
@ -604,9 +622,9 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
// 컬럼 토글 (현재 테이블 컬럼 - 입력용)
|
||||
const toggleInputColumn = (column: ColumnOption) => {
|
||||
const existingIndex = config.columns.findIndex((c) => c.key === column.columnName);
|
||||
const existingIndex = config.columns.findIndex((c) => c.key === column.columnName && !c.isJoinColumn && !c.isSourceDisplay);
|
||||
if (existingIndex >= 0) {
|
||||
const newColumns = config.columns.filter((c) => c.key !== column.columnName);
|
||||
const newColumns = config.columns.filter((_, i) => i !== existingIndex);
|
||||
updateConfig({ columns: newColumns });
|
||||
} else {
|
||||
// 컬럼의 inputType과 detailSettings 정보 포함
|
||||
|
|
@ -651,7 +669,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
const isColumnAdded = (columnName: string) => {
|
||||
return config.columns.some((c) => c.key === columnName && !c.isSourceDisplay);
|
||||
return config.columns.some((c) => c.key === columnName && !c.isSourceDisplay && !c.isJoinColumn);
|
||||
};
|
||||
|
||||
const isSourceColumnSelected = (columnName: string) => {
|
||||
|
|
@ -761,10 +779,9 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="basic" className="text-xs">기본</TabsTrigger>
|
||||
<TabsTrigger value="columns" className="text-xs">컬럼</TabsTrigger>
|
||||
<TabsTrigger value="entityJoin" className="text-xs">Entity 조인</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 기본 설정 탭 */}
|
||||
|
|
@ -1365,6 +1382,84 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* ===== 🆕 Entity 조인 컬럼 (표시용) ===== */}
|
||||
<div className="space-y-2 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 text-primary" />
|
||||
<Label className="text-xs font-medium text-primary">Entity 조인 컬럼 (읽기전용)</Label>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
FK 컬럼을 기반으로 참조 테이블의 데이터를 자동으로 조회하여 표시합니다.
|
||||
</p>
|
||||
|
||||
{loadingEntityJoins ? (
|
||||
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
||||
) : entityJoinData.joinTables.length === 0 ? (
|
||||
<p className="text-muted-foreground py-2 text-xs">
|
||||
{entityJoinTargetTable
|
||||
? `${entityJoinTargetTable} 테이블에 Entity 조인 가능한 컬럼이 없습니다`
|
||||
: "저장 테이블을 먼저 설정해주세요"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entityJoinData.joinTables.map((joinTable, tableIndex) => {
|
||||
const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || "";
|
||||
|
||||
return (
|
||||
<div key={tableIndex} className="space-y-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span>{joinTable.tableName}</span>
|
||||
<span className="text-muted-foreground">({sourceColumn})</span>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const isActive = isEntityJoinColumnActive(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
);
|
||||
const matchingCol = config.columns.find((c) => c.key === column.columnName && c.isJoinColumn);
|
||||
const displayField = matchingCol?.key || column.columnName;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10/50",
|
||||
isActive && "bg-primary/10",
|
||||
)}
|
||||
onClick={() =>
|
||||
toggleEntityJoinColumn(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
column.columnLabel,
|
||||
displayField,
|
||||
column.inputType || column.dataType
|
||||
)
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isActive}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 선택된 컬럼 상세 설정 - 🆕 모든 컬럼 통합, 순서 변경 가능 */}
|
||||
{config.columns.length > 0 && (
|
||||
<>
|
||||
|
|
@ -1381,7 +1476,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2",
|
||||
col.isSourceDisplay ? "border-primary/20 bg-primary/10/50" : "border-border bg-muted/30",
|
||||
(col.isSourceDisplay || col.isJoinColumn) ? "border-primary/20 bg-primary/10/50" : "border-border bg-muted/30",
|
||||
col.hidden && "opacity-50",
|
||||
)}
|
||||
draggable
|
||||
|
|
@ -1403,7 +1498,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
|
||||
|
||||
{/* 확장/축소 버튼 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedColumn(expandedColumn === col.key ? null : col.key)}
|
||||
|
|
@ -1419,8 +1514,10 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
{col.isSourceDisplay ? (
|
||||
<Link2 className="text-primary h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
||||
) : col.isJoinColumn ? (
|
||||
<Link2 className="text-amber-500 h-3 w-3 flex-shrink-0" title="Entity 조인 (읽기 전용)" />
|
||||
) : (
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<Input
|
||||
|
|
@ -1431,7 +1528,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
/>
|
||||
|
||||
{/* 히든 토글 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
|
||||
|
|
@ -1446,12 +1543,12 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
|
||||
{/* 자동입력 표시 아이콘 */}
|
||||
{!col.isSourceDisplay && col.autoFill?.type && col.autoFill.type !== "none" && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && col.autoFill?.type && col.autoFill.type !== "none" && (
|
||||
<Wand2 className="h-3 w-3 text-purple-500 flex-shrink-0" title="자동 입력" />
|
||||
)}
|
||||
|
||||
{/* 편집 가능 토글 */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateColumnProp(col.key, "editable", !(col.editable ?? true))}
|
||||
|
|
@ -1474,6 +1571,13 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
onClick={() => {
|
||||
if (col.isSourceDisplay) {
|
||||
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
||||
} else if (col.isJoinColumn) {
|
||||
const newColumns = config.columns.filter(c => c.key !== col.key);
|
||||
const newEntityJoins = config.entityJoins?.map(join => ({
|
||||
...join,
|
||||
columns: join.columns.filter(c => c.displayField !== col.key)
|
||||
})).filter(join => join.columns.length > 0);
|
||||
updateConfig({ columns: newColumns, entityJoins: newEntityJoins });
|
||||
} else {
|
||||
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
||||
}
|
||||
|
|
@ -1485,7 +1589,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && expandedColumn === col.key && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && expandedColumn === col.key && (
|
||||
<div className="ml-6 space-y-2 rounded-md border border-dashed border-input bg-muted p-2">
|
||||
{/* 자동 입력 설정 */}
|
||||
<div className="space-y-1">
|
||||
|
|
@ -1812,120 +1916,6 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Entity 조인 설정 탭 */}
|
||||
<TabsContent value="entityJoin" className="mt-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Entity 조인 연결</h3>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
FK 컬럼을 기반으로 참조 테이블의 데이터를 자동으로 조회하여 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-border" />
|
||||
|
||||
{loadingEntityJoins ? (
|
||||
<p className="text-muted-foreground py-2 text-center text-xs">로딩 중...</p>
|
||||
) : entityJoinData.joinTables.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-4 text-center">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{entityJoinTargetTable
|
||||
? `${entityJoinTargetTable} 테이블에 Entity 조인 가능한 컬럼이 없습니다`
|
||||
: "저장 테이블을 먼저 설정해주세요"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entityJoinData.joinTables.map((joinTable, tableIndex) => {
|
||||
const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || "";
|
||||
|
||||
return (
|
||||
<div key={tableIndex} className="space-y-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span>{joinTable.tableName}</span>
|
||||
<span className="text-muted-foreground">({sourceColumn})</span>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const isActive = isEntityJoinColumnActive(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
);
|
||||
const matchingCol = config.columns.find((c) => c.key === column.columnName);
|
||||
const displayField = matchingCol?.key || column.columnName;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10/50",
|
||||
isActive && "bg-primary/10",
|
||||
)}
|
||||
onClick={() =>
|
||||
toggleEntityJoinColumn(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
column.columnLabel,
|
||||
displayField,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isActive}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 현재 설정된 Entity 조인 목록 */}
|
||||
{config.entityJoins && config.entityJoins.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium">설정된 조인</h4>
|
||||
<div className="space-y-1">
|
||||
{config.entityJoins.map((join, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]">
|
||||
<Database className="h-3 w-3 text-primary" />
|
||||
<span className="font-medium">{join.sourceColumn}</span>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{join.referenceTable}</span>
|
||||
<span className="text-muted-foreground">
|
||||
({join.columns.map((c) => c.referenceField).join(", ")})
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
updateConfig({
|
||||
entityJoins: config.entityJoins!.filter((_, i) => i !== idx),
|
||||
});
|
||||
}}
|
||||
className="ml-auto h-4 w-4 p-0 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue